imagedata.formats.abstractplugin — imagedata 1.3.6 documentation

# Source code for imagedata.formats.abstractplugin

    """Abstract class for image formats.

    Defines generic functions.
    """

    # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
    # Copyright (c) 2017-2018 Erling Andersen, Haukeland University Hospital, Bergen, Norway

    from abc import ABCMeta, abstractmethod  # , abstractproperty
    import logging
    import numpy as np
    # import pydicom.dataset
    import imagedata.formats

    logger = logging.getLogger(__name__)

    class NoOtherInstance(Exception):
        pass

    class AbstractPlugin(object, metaclass=ABCMeta):
        """Abstract base class definition for imagedata format plugins.
        Plugins must be a subclass of AbstractPlugin and
        must define the atttributes set in __init__() and
        the following methods:

        read() method
        write_3d_numpy() method
        write_4d_numpy() method

        Attributes:
            input_order
            tags
            transformationMatrix
        """

        def __init__(self, name, description, authors, version, url):
            object.__init__(self)
            self.__name = name
            self.__description = description
            self.__authors = authors
            self.__version = version
            self.__url = url
            self.input_order = None
            self.tags = None
            self.transformationMatrix = None

        @property
        def name(self):
            """Plugin name

            Single word string describing the image format.
            Typical names: dicom, nifti, itk.
            """
            return self.__name

        @property
        def description(self):
            """Plugin description

            Single line string describing the image format.
            """
            return self.__description

        @property
        def authors(self):
            """Plugin authors

            Multi-line string naming the author(s) of the plugin.
            """
            return self.__authors

        @property
        def version(self):
            """Plugin version

            String giving the plugin version.
            Version scheme: 1.0.0
            """
            return self.__version

        @property
        def url(self):
            """Plugin URL

            URL string to the site of the plugin or the author(s).
            """
            return self.__url

        def read(self, sources, pre_hdr, input_order, opts):
            """Read image data

            Generic version for images which will be sorted on their filenames.

            Args:
                sources: list of sources to image data
                pre_hdr: DICOM template
                input_order: sort order
                opts: Input options (dict)
            Returns:
                Tuple of
                    - self: Header dict
                    - si[tag,slice,rows,columns]: numpy array
            """

            self = {
                'input_format': self.name,
                'input_order': input_order
            }

            # image_list: list of tuples (self,si)
            logger.debug("AbstractPlugin.read: sources {}".format(sources))
            image_list = list()
            for source in sources:
                logger.debug("AbstractPlugin.read: source: {} {}".format(type(source), source))
                archive = source['archive']
                scan_files = source['files']
                if scan_files is None or len(scan_files) == 0:
                    scan_files = archive.getnames()
                # logger.debug("AbstractPlugin.read: scan_files {}".format(scan_files))
                for file_handle in archive.getmembers(scan_files):
                    logger.debug("AbstractPlugin.read: file_handle {}".format(file_handle))
                    if self._need_local_file():
                        logger.debug("AbstractPlugin.read: need local file {}".format(file_handle))
                        f = archive.to_localfile(file_handle)
                        logger.debug("AbstractPlugin.read: local file {}".format(f))
                        info, si = self._read_image(f, opts, self)
                    else:
                        f = archive.open(file_handle, mode='rb')
                        logger.debug("AbstractPlugin.read: file {}".format(f))
                        info, si = self._read_image(f, opts, self)
                        f.close()
                    # info is None when no image was read
                    if info is not None:
                        image_list.append((info, si))
            if len(image_list) < 1:
                raise ValueError('No image data read')
            info, si = image_list[0]
            self._reduce_shape(si)
            logger.debug('AbstractPlugin.read: reduced si {}'.format(si.shape))
            shape = (len(image_list),) + si.shape
            dtype = si.dtype
            logger.debug('AbstractPlugin.read: shape {}'.format(shape))
            si = np.zeros(shape, dtype)
            i = 0
            for info, img in image_list:
                # logger.debug('AbstractPlugin.read: img {} si {} {}'.format(img.shape, si.shape, si.dtype))
                si[i] = img
                i += 1
            logger.debug('AbstractPlugin.read: si {}'.format(si.shape))

            # Simplify shape
            self._reduce_shape(si)
            logger.debug('AbstractPlugin.read: reduced si {}'.format(si.shape))

            _shape = si.shape
            if 'color' in self and self['color']:
                _shape = si.shape[:-1]
                logger.debug('AbstractPlugin.read: color')
            logger.debug('AbstractPlugin.read: _shape {}'.format(_shape))
            _ndim = len(_shape)
            nz = 1
            # ny, nx = _shape[-2:]
            if _ndim > 2:
                nz = _shape[-3]
            # if _ndim > 3:
            #     nt = _shape[-4]
            # self['slices'] = nz
            logger.debug('AbstractPlugin.read: slices {}'.format(nz))

            # self['spacing'], self['tags']
            logger.debug('AbstractPlugin.read: calling _set_tags')
            self._set_tags(image_list, self, si)
            # logger.debug('AbstractPlugin.read: return  _set_tags: {}'.format(self))

            logger.info("Data shape read: {}".format(imagedata.formats.shape_to_str(si.shape)))

            # Add any DICOM template
            if pre_hdr is not None:
                self.update(pre_hdr)

            logger.debug('AbstractPlugin.read: self {}'.format(
                self.keys()))
            # logger.debug('AbstractPlugin.read: self {}'.format(self))
            return self, si

        def _need_local_file(self):
            """Do the plugin need access to local files?

            Returns:
                Boolean
                    - True: The plugin need access to local filenames
                    - False: The plugin can access files given by an open file handle
            """

            return False

        @abstractmethod
        def _read_image(self, f, opts, self):
            """Read image data from given file handle

            Args:
                self: format plugin instance
                f: file handle or filename (depending on self._need_local_file)
                opts: Input options (dict)
                self: Header dict
            Returns:
                Tuple of
                    self: Header dict
                        Return values:
                            - info: Internal data for the plugin
                              None if the given file should not be included (e.g. raw file)
                    si: numpy array (multi-dimensional)
            """
            pass

        @abstractmethod
        def _set_tags(self, image_list, self, si):
            """Set header tags.

            Args:
                self: format plugin instance
                image_list: list with (info,img) tuples
                self: Header dict
                si: numpy array (multi-dimensional)
            Returns:
                self: Header dict
            """
            pass

        @abstractmethod
        def write_3d_numpy(self, si, destination, opts):
            """Write 3D Series image

            Args:
                self: format plugin instance
                si[slice,rows,columns]: Series array
                destination: dict of archive and filenames
                opts: Output options (dict)
            """
            pass

        @abstractmethod
        def write_4d_numpy(self, si, destination, opts):
            """Write 4D Series image

            Args:
                self: format plugin instance
                si[tag,slice,rows,columns]: Series array
                destination: dict of archive and filenames
                opts: Output options (dict)
            """
            pass

        def getTimeline(self):
            """Get timeline

            Returns:
                Timeline in seconds, as numpy array of floats
                    Delta time is given as seconds. First image is t=0.
                    Length of array is number of tags.
            Raises:
                ValueError: tags for dataset is not time tags
            """
            if self.input_order == imagedata.formats.INPUT_ORDER_TIME:
                timeline = [0.0]
                for t in range(1, len(self.tags[0])):
                    timeline.append(self.tags[0][t] - self.tags[0][0])
                return np.array(timeline)
            else:
                raise ValueError("No timeline tags are available. Input order: {}".format(self.input_order))

        '''
        def getQform(self):
            """Get Nifti version of the transformation matrix, aka qform

            Input:
            - self.spacing
            - self.imagePositions
            - self.orientation
            Returns:
            - transformation matrix as numpy array
            """

            def normalize_column(x,row):
                val = np.vdot(x, x)

                if val > 0:
                    x = x / math.sqrt(val)
                else:
                    shape = x.shape
                    x = np.array([0., 0., 0.])
                    x[row]=1
                    x.shape = shape
                return x

            #debug = None
            debug = True

            ds,dr,dc    = self.spacing
            z,y,x       = self.imagePositions[0]
            slices      = len(self.imagePositions)
            T0          = self.imagePositions[0]
            Tn          = self.imagePositions[slices-1]
            orient      = self.orientation
            #print("ds,dr,dc={},{},{}".format(ds,dr,dc))
            #print("z ,y ,x ={},{},{}".format(z,y,x))

            q = np.eye(4)
            # Set column 3 and row 3 to zeros, except [3,3]
            colr=np.array([[orient[3]], [orient[4]], [orient[5]]])
            colc=np.array([[orient[0]], [orient[1]], [orient[2]]])
            colr = normalize_column(colr,0)
            colc = normalize_column(colc,1)
            k=np.cross(colr, colc, axis=0)

            q[:3, :3] = np.hstack((colr, colc, k))
            if debug:
                logger.debug("q")
                logger.debug( q)

            if debug:
                logger.debug("determinant(q) {}".format(np.linalg.det(q)))
            if np.linalg.det(q) < 0:
                q[:3,2] = -q[:3,2]

            # Scale matrix
            diagVox = np.eye(3)
            diagVox[0,0] = dc
            diagVox[1,1] = dr
            diagVox[2,2] = ds
            if debug:
                logger.debug("diagVox")
                logger.debug( diagVox)
                logger.debug("q without scaling {}".format(q.dtype))
                logger.debug( q)
            q[:3,:3] = np.dot(q[:3,:3],diagVox)
            if debug:
                logger.debug("q with scaling {}".format(q.dtype))
                logger.debug( q)

            # Add translations
            q[0,3] = x; q[1,3] = y; q[2,3] = z       # pos x,y,z
            if debug:
                logger.debug("q with translations")
                logger.debug( q)
            # q now equals dicom_to_patient in spm_dicom_convert

            return q
        '''

        '''
        def setQform(self, A):
            """Set transformationMatrix from Nifti affine A."""
            #print("setQform:  input\n{}".format(A))
            M=np.eye(4)
            M[:3,0]=A[2::-1,2]
            M[:3,1]=A[2::-1,0]
            M[:3,2]=A[2::-1,1]
            M[:3,3]=A[2::-1,3]
            #print("setQform: output\n{}".format(M))
            self.transformationMatrix=M
            return
        '''

        def getPositionForVoxel(self, r, transformation=None):
            """Get patient position for center of given voxel r

            Use Patient Position and Image Orientation to calculate world
            coordinates for given voxel

            Args:
                r: (z,y,x) of voxel in voxel coordinates as numpy.array
                transformation: transformation matrix when different from self.transformationMatrix
            Returns:
                (z,y,x) of voxel in world coordinates (mm) as numpy.array
            """

            if transformation is None:
                transformation = self.transformationMatrix
            # q = self.getTransformationMatrix()

            # V = np.array([[r[2]], [r[1]], [r[0]], [1]])  # V is [x,y,z,1]

            newpos = np.dot(transformation, np.hstack((r, [1])))

            # return np.array([newpos[2,0],newpos[1,0],newpos[0,0]])   # z,y,x
            return newpos[:3]

        def getVoxelForPosition(self, p, transformation=None):
            """ Get voxel for given patient position p

            Use Patient Position and Image Orientation to calculate world
            coordinates for given voxel

            Args:
                p: (z,y,x) of voxel in world coordinates (mm) as numpy.array
                transformation: transformation matrix when different from self.transformationMatrix
            Returns:
                (z,y,x) of voxel in voxel coordinates as numpy.array
            """

            if transformation is None:
                transformation = self.transformationMatrix
            # q = self.getTransformationMatrix()

            # V = np.array([[p[2]], [p[1]], [p[0]], [1]])    # V is [x,y,z,1]

            qinv = np.linalg.inv(transformation)
            r = np.dot(qinv, np.hstack((p, [1])))

            # z,y,x
            # return np.array([int(r[2,0]+0.5),int(r[1,0]+0.5),int(r[0,0]+0.5)], dtype=int)
            # return int(r+0.5)[:3]
            return (r + 0.5).astype(int)[:3]

        @staticmethod
        def replace_geometry_attributes(im, gim):
            """Replace geometry attributes in image with values from gim
            """

            im.SliceLocation = gim.SliceLocation
            im.ImagePositionPatient = gim.ImagePositionPatient
            im.ImageOrientationPatient = gim.ImageOrientationPatient
            im.FrameOfReferenceUID = gim.FrameOfReferenceUID
            im.PositionReferenceIndicator = gim.PositionReferenceIndicator
            im.SliceThickness = gim.SliceThickness
            try:
                im.SpacingBetweenSlices = gim.SpacingBetweenSlices
            except AttributeError:
                pass
            im.AcquisitionMatrix = gim.AcquisitionMatrix
            im.PixelSpacing = gim.PixelSpacing

        @staticmethod
        def _reduce_shape(si, axes=None):
            """Reduce shape when leading shape(s) are 1.

            Will not reduce to less than 2-dimensional image.
            Also reduce axes when reducing shape.

            Args:
                self: format plugin instance
                si[...]: Series array
            Raises:
                ValueError: tags for dataset is not time tags
            """

            # Color image?
            mindim = 2
            if si.shape[-1] == 3:
                mindim += 1

            while si.ndim > mindim:
                if si.shape[0] == 1:
                    si.shape = si.shape[1:]
                    if axes is not None:
                        del axes[0]
                else:
                    break

        def _reorder_to_dicom(self, data, flip=False, flipud=False):
            """Reorder data to internal DICOM format.

            Swap axes, except for rows and columns.

            5D:
            data  order: data[rows,columns,slices,tags,d5]
            DICOM order: si  [d5,tags,slices,rows,columns]

            4D:
            data  order: data[rows,columns,slices,tags]
            DICOM order: si  [tags,slices,rows,columns]

            3D:
            data  order: data[rows,columns,slices]
            DICOM order: si  [slices,rows,columns]

            2D:
            data  order: data[rows,columns]
            DICOM order: si  [rows,columns]

            flip: Whether rows and columns are swapped.
            flipud: Whether matrix is transposed
            """

            logger.debug('AbstractPlugin._reorder_to_dicom: shape in {}'.format(
                data.shape))
            if data.ndim == 5:
                rows, columns, slices, tags, d5 = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((d5, tags, slices, rows, columns), data.dtype)
                for d in range(d5):
                    for tag in range(tags):
                        for slice in range(slices):
                            si[d, tag, slice, :, :] = self._reorder_slice(data[:, :, slice, tag, d], flip=flip,
                                                                          flipud=flipud)
                            # if flip:
                            #    si[d,tag,slice,:,:] = \
                            #    (data[:,:,slice,tag,d]).T
                            #    #np.fliplr(data[:,:,slice,tag,d]).T
                            # else:
                            #    si[d,tag,slice,:,:] = data[:,:,slice,tag,d]
            elif data.ndim == 4:
                rows, columns, slices, tags = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((tags, slices, rows, columns), data.dtype)
                for tag in range(tags):
                    for slice in range(slices):
                        si[tag, slice, :, :] = self._reorder_slice(data[:, :, slice, tag], flip=flip, flipud=flipud)
                        # if flip:
                        #    si[tag,slice,:,:] = (data[:,:,slice,tag]).T
                        #    #si[tag,slice,:,:] = np.fliplr(data[:,:,slice,tag]).T
                        # else:
                        #    si[tag,slice,:,:] = data[:,:,slice,tag]
            elif data.ndim == 3:
                rows, columns, slices = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((slices, rows, columns), data.dtype)
                for slice in range(slices):
                    si[slice, :, :] = self._reorder_slice(data[:, :, slice], flip=flip, flipud=flipud)
                    # if flip:
                    #    si[slice,:,:] = (data[:,:,slice]).T
                    #    #si[slice,:,:] = np.fliplr(data[:,:,slice]).T
                    # else:
                    #    si[slice,:,:] = data[:,:,slice]
            elif data.ndim == 2:
                rows, columns = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((rows, columns), data.dtype)
                si[:] = self._reorder_slice(data[:], flip=flip, flipud=flipud)
                # if flip:
                #    si[:] = (data[:]).T
                #    #si[:] = np.fliplr(data[:]).T
                # else:
                #    si[:] = data[:]
            else:
                raise ValueError('Dimension %d is not implemented' % data.ndim)
            logger.debug('AbstractPlugin._reorder_to_dicom: shape out {}'.format(
                si.shape))
            return si

        def _reorder_from_dicom(self, data, flip=False, flipud=False):
            """Reorder data from internal DICOM format.

            Swap axes, except for rows and columns.

            5D:
            DICOM  order: data[d5,tags,slices,rows,columns]
            return order: si  [rows,columns,slices,tags,d5]

            4D:
            DICOM  order: data[tags,slices,rows,columns]
            return order: si  [rows,columns,slices,tags]

            3D:
            DICOM  order: data[slices,rows,columns]
            return order: si  [rows,columns,slices]

            2D:
            DICOM  order: data[rows,columns]
            return order: si [rows,columns]

            flip: Whether rows and columns are swapped.
            flipud: Whether matrix is transposed
            """

            logger.debug('AbstractPlugin._reorder_from_dicom: shape in {}'.format(data.shape))
            if data.ndim == 5:
                d5, tags, slices, rows, columns = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((rows, columns, slices, tags, d5), data.dtype)
                for d in range(d5):
                    for tag in range(tags):
                        for slice in range(slices):
                            si[:, :, slice, tag, d] = self._reorder_slice(data[d, tag, slice, :, :], flip=flip,
                                                                          flipud=flipud)
                            # if flip:
                            #    si[:,:,slice,tag,d] = \
                            #    np.fliplr(data[d,tag,slice,:,:]).T
                            # else:
                            #    si[:,:,slice,tag,d] = data[d,tag,slice,:,:]
            elif data.ndim == 4:
                tags, slices, rows, columns = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((rows, columns, slices, tags), data.dtype)
                for tag in range(tags):
                    for slice in range(slices):
                        si[:, :, slice, tag] = self._reorder_slice(data[tag, slice, :, :], flip=flip, flipud=flipud)
                        # if flip:
                        #    si[:,:,slice,tag] = np.fliplr(data[tag,slice,:,:]).T
                        # else:
                        #    si[:,:,slice,tag] = data[tag,slice,:,:]
            elif data.ndim == 3:
                slices, rows, columns = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((rows, columns, slices), data.dtype)
                for slice in range(slices):
                    si[:, :, slice] = self._reorder_slice(data[slice, :, :], flip=flip, flipud=flipud)
                    # if flip:
                    #    si[:,:,slice] = np.fliplr(data[slice,:,:]).T
                    # else:
                    #    si[:,:,slice] = data[slice,:,:]
            elif data.ndim == 2:
                rows, columns = data.shape
                if flipud:
                    rows, columns = columns, rows
                si = np.zeros((rows, columns), data.dtype)
                si[:, :] = self._reorder_slice(data[:, :], flip=flip, flipud=flipud)
                # if flip:
                #    si[:] = np.fliplr(data[:]).T
                # else:
                #    si[:] = data[:]
            else:
                raise ValueError('Dimension %d is not implemented' % data.ndim)
            logger.debug('AbstractPlugin._reorder_from_dicom: shape out {}'.format(
                si.shape))
            return si

        @staticmethod
        def _reorder_slice(data, flip, flipud):
            if flip and flipud:
                return np.fliplr(data).T
            elif flip:
                return np.fliplr(data)
            elif flipud:
                return data.T
            else:
                return data