Module ctsimu.image

Single image handling and processing.

  • Image reads, stores, writes and handles image data.
  • ImageFile gathers information about an image file: file name, data type, byte order. It is used to instruct the Image.read() and Image.save() routines.
  • ImageStack represents a stack of images in the file system. It can be used in combination with a processing pipeline (see ctsimu.processing).
  • ImageROI defines a pixel region of interest in an image.

Images

To import a single image, you can specify its file name in the constructor and then use the Image.read() function to import it into the internal memory. It will be stored in self._px as a float64 NumPy array. When writing an image using Image.save(), you have to specify the data type for the new file.

from ctsimu.image import Image

myImage = Image("example.tif")
myImage.read()

# Mirror horizontally:
myImage.flipHorizontal()

myImage.save("example_mirrored.raw", dataType="float32")

Raw File Handling

To read raw image data, its dimensions, data type, byte order and header size must be specified:

from ctsimu.image import Image

myImage = Image("example_mirrored.raw")
myImage.read(width=501,
             height=501,
             dataType="float32",
             byteOrder="little",
             fileHeaderSize=0)

# Export as big endian, uint16:
myImage.save("example_converted.raw",
             dataType="uint16",
             byteOrder="big")

Functions

def createImageStack(stack)

Return an ImageStack object, if string is given.

def isTIFF(filename: str) ‑> bool

Check if file name signifies a TIFF image.

Classes

class Image (inputFile=None, outputFile=None)

Stores pixel data, provides image processing routines.

Expand source code
class Image:
    """ Stores pixel data, provides image processing routines. """

    def __init__(self, inputFile=None, outputFile=None):
        self.inputFile  = None   # type ImageFile or string
        self.outputFile = None   # type ImageFile or string
        self.px         = 0  # 2D numpy array that contains the pixel values.
        self.height     = 0  # Image height in px.
        self.width      = 0  # Image width in px.
        self.index      = 0  # Slice number in a 3D volume.

        self.rotation     = None
        self.flipHorz     = False
        self.flipVert     = False

        self.n_accumulations = 0   # Counts number of accumulated pictures for averaging (mean)
        self.boundingBoxX0   = 0   # After cropping: bounding box offset relative to original image.
        self.boundingBoxY0   = 0
        self.resolution      = 1   # After binning: new resolution relative to original image.

        self.setInputFile(inputFile)
        self.setOutputFile(outputFile)

    def __add__(self, other):
        if self.dimensionsMatch(other):
            result = copy.deepcopy(self)
            result.px += other.px
            return result
        else:
            raise Exception("Cannot add images of different dimensions.")

    def __sub__(self, other):
        if self.dimensionsMatch(other):
            result = copy.deepcopy(self)
            result.px -= other.px
            return result
        else:
            raise Exception("Cannot subtract images of different dimensions.")

    def __mul__(self, other):
        if self.dimensionsMatch(other):
            result = copy.deepcopy(self)
            result.px *= other.px
            return result
        else:
            raise Exception("Cannot multiply images of different dimensions.")

    def __truediv__(self, other):
        if self.dimensionsMatch(other):
            result = copy.deepcopy(self)
            result.px[numpy.nonzero(other.px)] /= other.px[numpy.nonzero(other.px)]
            result.px = numpy.where(other.px==0, 0, result.px)
            return result
        else:
            raise Exception("Cannot divide images of different dimensions.")

    def __floordiv__(self, other):
        if self.dimensionsMatch(other):
            result = copy.deepcopy(self)
            result.px[numpy.nonzero(other.px)] //= other.px[numpy.nonzero(other.px)]
            result = numpy.where(other.px==0, 0, result.px)
            return result
        else:
            raise Exception("Cannot divide images of different dimensions.")

    def __del__(self):
        """ Delete pixel map upon object destruction. """
        self.px =0

    def setInputFile(self, inputFile):
        """ Set input file properties from ImageFile object or string. """
        if isinstance(inputFile, ImageFile) or (inputFile is None):
            self.inputFile = inputFile
        elif isinstance(inputFile, str):  # string given
            self.inputFile = ImageFile(inputFile)
        else:
            raise Exception("{} is not a valid file identifier.")

    def setOutputFile(self, outputFile):
        """ Set output file properties from ImageFile object or string. """
        if isinstance(outputFile, ImageFile) or (outputFile is None):
            self.outputFile = outputFile
        elif isinstance(outputFile, str):  # string given
            self.outputFile = ImageFile(outputFile)
        else:
            raise Exception("{} is not a valid file identifier.")

    def setHeight(self, height):
        """ Set image height in px. """
        self.height = height

    def setWidth(self, width):
        """ Set image width in px. """
        self.width = width

    def setIndex(self, index):
        """ Set image index position in 3D stack (in px). """
        self.index = index

    def shape(self, width, height, index=0, dataType=None, value=0):
        """ Re-format image to given dimensions and data type. """
        self.setWidth(width)
        self.setHeight(height)
        self.setIndex(index)

        if dataType is None:
            dataType = self.getInternalDataType()

        self.erase(value=0, dataType=dataType)

    def shapeLike(self, otherImg, dataType=None):
        self.setWidth(otherImg.getWidth())
        self.setHeight(otherImg.getHeight())
        self.setIndex(otherImg.getIndex())

        if dataType is None:
            dataType = otherImg.getInternalDataType()

        self.erase(value=0, dataType=dataType)

    def erase(self, value=0, dataType=None):
        """ Set all pixels to 'value'. """
        w = self.getWidth()
        h = self.getHeight()

        if dataType is None:
            dataType = self.getInternalDataType()

        self.px = 0
        self.px = numpy.full((h, w), fill_value=value, dtype=dataType)

    def getPixelMap(self):
        return self.px

    def setPixelMap(self, px):
        self.px = px

    def setPixel(self, x, y, value):
        self.px[y][x] = value

    def getPixel(self, x, y):
        return self.px[y][x]

    def isSet(self):
        """ Check if image has a valid width and height. """
        if(self.getHeight() > 0):
            if(self.getWidth() > 0):
                return True

        return False

    def contains(self, x, y):
        """ Check if (x, y) is within image dimensions. """
        if x >= 0:
            if y >= 0:
                if x < self.getWidth():
                    if y < self.getHeight():
                        return True

        return False

    def getWidth(self):
        return self.width

    def getHeight(self):
        return self.height

    def getNPixels(self):
        """ Calculate number of pixels in image. """
        return (self.getWidth() * self.getHeight())

    def getIndex(self):
        return self.index

    def getBoundingBoxX0(self):
        return self.boundingBoxX0

    def getBoundingBoxY0(self):
        return self.boundingBoxY0

    def getResolution(self):
        return self.resolution

    def getFileByteOrder(self):
        return self.fileByteOrder

    def max(self, ROI=None):
        """ Return maximum intensity in image. """

        # Take full image if no ROI is given
        if ROI is None:
            return numpy.amax(self.px)

        return numpy.amax(self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1])

    def min(self, ROI=None):
        """ Return minimum intensity in image. """

        # Take full image if no ROI is given
        if ROI is None:
            return numpy.amin(self.px)

        return numpy.amin(self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1])

    def mean(self, ROI=None):
        """ Return arithmetic mean of the image grey values. """

        # Take full image if no ROI is given
        if ROI is None:
            return numpy.mean(self.px)

        return numpy.mean(self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1])

    def stdDev(self, ROI=None):
        """ Return the standard deviation of the image grey values. """

        # Take full image if no ROI is given
        if ROI is None:
            return numpy.std(self.px)

        return numpy.std(self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1])

    def centerOfMass(self):
        return ndimage.center_of_mass(self.px)

    def setRotation(self, rotation):
        self.rotation = rotation

    def getRotation(self):
        return self.rotation

    def rot90(self):
        if self.isSet():
            self.px = numpy.require(numpy.rot90(self.px, k=1), requirements=['C_CONTIGUOUS'])
            self.width, self.height = self.height, self.width

    def rot180(self):
        if self.isSet():
            self.px = numpy.require(numpy.rot90(self.px, k=2), requirements=['C_CONTIGUOUS'])

    def rot270(self):
        if self.isSet():
            self.px = numpy.require(numpy.rot90(self.px, k=-1), requirements=['C_CONTIGUOUS'])
            self.width, self.height = self.height, self.width

    def rotate(self, rotation):
        if rotation is None:
            rotation = self.rotation
        else:
            self.setRotation(rotation)

        if rotation == "90":
            self.rot90()
        elif rotation == "180":
            self.rot180()
        elif rotation == "270":
            self.rot270()

    def flipHorizontal(self):
        self.flipHorz = not self.flipHorz
        if self.isSet():
            self.px = numpy.require(numpy.fliplr(self.px), requirements=['C_CONTIGUOUS'])

    def flipVertical(self):
        self.flipVert = not self.flipVert
        if self.isSet():
            self.px = numpy.require(numpy.flipud(self.px), requirements=['C_CONTIGUOUS'])

    def setFlip(self, horz=False, vert=False):
        self.flipHorz = horz
        self.flipVert = vert

    def getHorizontalFlip(self):
        return self.flipHorz

    def getVerticalFlip(self):
        return self.flipVert

    def flip(self, horizontal=False, vertical=False):
        if horizontal:
            self.flipHorizontal()
        if vertical:
            self.flipVertical()

    def getInternalDataType(self):
        """ Data type used internally for all image data. """
        return numpy.dtype('float64')

    def containsPixelValue(self, value):
        """ Check if image contains a certain grey value. """
        return numpy.any(self.px == value)

    def dimensionsMatch(self, img):
        """ Check if image dimensions match with another image. """
        if self.isSet() and img.isSet():
            if(self.getHeight() == img.getHeight()):
                if(self.getWidth() == img.getWidth()):
                    return True

        raise Exception("Pixel dimensions do not match: {}x{} vs. {}x{}".format(self.getWidth(), self.getHeight(), img.getWidth(), img.getHeight()))

        return False

    def read(self, filename=None, width=None, height=None, index=0, dataType=None, byteOrder=None, fileHeaderSize=0, imageHeaderSize=0):
        """ Read TIFF or RAW, decide by file name. """
        if filename is None:
            filename = self.inputFile.getFilename()
        else:
            self.setInputFile(filename)

        # If no internal file name is specified, do nothing.
        if filename is None:
            return

        if isTIFF(self.inputFile.getFilename()):
            self.readTIFF(self.inputFile.doFlipByteOrder())
        else:
            self.readRAW(width=width, height=height, index=index, dataType=dataType, byteOrder=byteOrder, fileHeaderSize=fileHeaderSize, imageHeaderSize=imageHeaderSize)

    def readTIFF(self, flipByteOrder=False, obeyOrientation=True):
        """ Import TIFF file. """
        if os.path.isfile(self.inputFile.getFilename()):
            basename = self.inputFile.getFileBasename()

            tiffimg = tiff()
            tiffimg.read(self.inputFile.getFilename())
            img = tiffimg.imageData(subfile=0, channel=0, obeyOrientation=obeyOrientation)  # get a greyscale image from TIFF subfile 0
            width = tiffimg.getWidth(subfile=0)
            height = tiffimg.getHeight(subfile=0)

            self.inputFile.setDataType(img.dtype)

            if flipByteOrder:
                img.byteswap(inplace=True)

            # Convert to internal data type for either int or float:
            self.px = img.astype(self.getInternalDataType())

            # Check if array in memory has the dimensions stated in the TIFF file:
            if((height == len(self.px)) and (width == len(self.px[0]))):
                self.setHeight(height)
                self.setWidth(width)
            else:
                raise Exception("Width ({}px) and height ({}px) from the TIFF header do not match the data width ({}px) and height ({}px) that has been read.".format(width, height, len(self.px[0]), len(self.px)))
        else:
            raise Exception("Can't find " + self.inputFile.getFilename())

    def readRAW(self, width, height, index=0, dataType=None, byteOrder=None, fileHeaderSize=0, imageHeaderSize=0):
        """ Import RAW image file. """
        if not isinstance(self.inputFile, ImageFile):
            raise Exception("No valid input file defined.")

        if dataType is None:
            dataType = self.inputFile.getDataType()
        else:
            self.inputFile.setDataType(dataType)

        if byteOrder is None:
            byteOrder = self.inputFile.getByteOrder()
            if byteOrder is None:
                byteOrder = sys.byteorder

        self.inputFile.setByteOrder(byteOrder)

        if os.path.isfile(self.inputFile.getFilename()):
            self.shape(width, height, index, self.inputFile.getDataType())

            basename = self.inputFile.getFileBasename()
            #log("Reading RAW file {}...".format(basename))

            byteOffset = fileHeaderSize + (index+1)*imageHeaderSize + index*(self.getNPixels() * self.inputFile.getDataType().itemsize)

            with open(self.inputFile.getFilename(), 'rb') as f:
                f.seek(byteOffset)
                self.px = numpy.fromfile(f, dtype=self.inputFile.getDataType(), count=self.getNPixels(), sep="")

            if len(self.px) > 0:
                # Treat endianness. If the native byte order of the system is different
                # than the given file byte order, the bytes are swapped in memory
                # so that it matches the native byte order.
                nativeEndian = sys.byteorder
                if nativeEndian == 'little':
                    if byteOrder == 'big':
                        self.px.byteswap(inplace=True)
                elif nativeEndian == 'big':
                    if byteOrder == 'little':
                        self.px.byteswap(inplace=True)

                # Convert to internal data type:
                self.px = self.px.astype(self.getInternalDataType())

                # Reshape to 2D array:
                self.px = numpy.reshape(self.px, (height, width))
            else:
                raise Exception("Error reading RAW file {f}.\nGot no data for index {idx}.".format(f=self.inputFile.getFilename(), idx=index))

        else:
            raise Exception("Can't find " + self.inputFile.getFilename())

    def getDataTypeClippingBoundaries(self, dataType):
        # Get clipping boundaries if grey values have to be
        # clipped to the interval supported by the int image type:
        clipMin = 0
        clipMax = 1
        if numpy.issubdtype(dataType, numpy.integer):
            intInfo   = numpy.iinfo(dataType)
            clipMin   = intInfo.min
            clipMax   = intInfo.max
        elif numpy.issubdtype(dataType, numpy.floating):
            floatInfo = numpy.finfo(dataType)
            clipMin   = floatInfo.min
            clipMax   = floatInfo.max

        return clipMin, clipMax

    def save(self, filename=None, dataType=None, byteOrder=None, appendChunk=False, clipValues=True):
        """ Save image as TIFF or RAW. """
        if not isinstance(self.outputFile, ImageFile):
            self.outputFile = ImageFile()

        if (filename is None) or (filename == ""):
            filename = self.outputFile.getFilename()
            if (filename is None) or (filename == ""):
                raise Exception("No output file name specified.")
        else:
            self.outputFile.setFilename(filename)

        if dataType is None:
            dataType = self.outputFile.getDataType()
            if dataType is None:
                if isinstance(self.inputFile, ImageFile):
                    dataType = self.inputFile.getDataType()
                    if(dataType is not None):
                        self.outputFile.setDataType(dataType)
                    else:
                        raise Exception("Please specify a data type for the output file: {filename}".format(filename=filename))
                else:
                    raise Exception("Please specify a data type for the output file: {filename}".format(filename=filename))
        else:
            self.outputFile.setDataType(dataType)

        if byteOrder is None:
            byteOrder = self.outputFile.getByteOrder()
            if byteOrder is None:
                if isinstance(self.inputFile, ImageFile):
                    byteOrder = self.inputFile.getByteOrder()
                    self.outputFile.setByteOrder(byteOrder)

            if byteOrder is None:
                byteOrder = "little"

        self.outputFile.setByteOrder(byteOrder)

        if isTIFF(filename):
            self.saveTIFF(filename, dataType, clipValues)
        else:
            self.saveRAW(filename, dataType, byteOrder, appendChunk, clipValues, addInfo=False)

    def saveTIFF(self, filename=None, dataType=None, clipValues=True):
        if (filename is not None) and (len(filename) > 0):
            fileBaseName = os.path.basename(filename)
            if (fileBaseName == "") or (fileBaseName is None):
                raise Exception("No output file name specified for the image to be saved.")

            if dataType is not None:
                if not isTIFF(filename):
                    filename += ".tif"

                touch_directory(filename)

                tiffdata = None
                if clipValues:  # Clipping
                    clipMin, clipMax = self.getDataTypeClippingBoundaries(dataType)
                    tiffdata = numpy.clip(self.px, clipMin, clipMax).astype(dataType)
                else:  # No clipping or float
                    tiffdata = self.px.astype(dataType)

                tiffimg = tiff()
                tiffimg.set(tiffdata)
                tiffimg.save(filename=filename, endian='little')
            else:
                raise Exception("Please specify a data type for the output file: {filename}".format(filename=filename))
        else:
            raise Exception("No output file name specified for the image to be saved.")

    def saveRAW(self, filename=None, dataType=None, byteOrder=None, appendChunk=False, clipValues=True, addInfo=False):
        if (filename is not None) and (len(filename) > 0):
            fileBaseName = os.path.basename(filename)
            if (fileBaseName == "") or (fileBaseName is None):
                raise Exception("No output file name specified for the image to be saved.")

            if dataType is not None:
                if byteOrder is None:
                    byteOrder = "little"

                # Reshape to 1D array and convert to file data type (from internal 64bit data type)
                outBytes = numpy.reshape(self.px, int(self.width)*int(self.height))

                if clipValues:  # Clipping
                    clipMin, clipMax = self.getDataTypeClippingBoundaries(dataType)
                    outBytes = numpy.clip(outBytes, clipMin, clipMax)

                outBytes = outBytes.astype(dataType)

                # Treat endianness. If the native byte order of the system is different
                # than the desired file byte order, the bytes are swapped in memory
                # before writing to disk.
                nativeEndian = sys.byteorder
                if nativeEndian == 'little':
                    if byteOrder  == 'big':
                        outBytes.byteswap(inplace=True)
                elif nativeEndian == 'big':
                    if byteOrder == 'little':
                        outBytes.byteswap(inplace=True)

                if addInfo:
                    shortEndian = "LE"
                    if byteOrder == "big":
                        shortEndian = "BE"

                    infoString = "_{width}x{height}_{dataType}_{endian}".format(width=self.width, height=self.height, dataType=dataType, endian=shortEndian)

                    basename, extension = os.path.splitext(filename)
                    filename = basename + infoString + extension

                touch_directory(filename)
                if not appendChunk:  # save as single raw file
                    with open(filename, 'w+b') as file:
                        file.write(outBytes)
                        file.close()
                    #outBytes.tofile(filename, sep="")
                else: # append to the bytes of the chunk file
                    with open(filename, 'a+b') as file:
                        file.write(outBytes)
                        file.close()
            else:
                raise Exception("Please specify a data type for the output file: {filename}".format(filename=filename))
        else:
            raise Exception("No output file name specified for the image to be saved.")

    def calcRelativeShift(self, referenceImage):
        if self.dimensionsMatch(referenceImage):
            # Convolution of this pixmap with the vertically and horizontally mirrored reference pixmap
            img1 = self.px - int(numpy.mean(self.px))
            img2 = referenceImage.getPixelMap() - numpy.mean(referenceImage.getPixelMap())

            convolution = signal.fftconvolve(img1, img2[::-1,::-1], mode='same')

            maximum = numpy.unravel_index(numpy.argmax(convolution), convolution.shape)

            return (maximum[1] - self.getWidth()/2, maximum[0] - self.getHeight()/2)
        else:
            raise Exception("Dimensions of image ({}, {}) and reference image ({}, {}) must match for convolution.".format(self.getWidth(), self.getHeight(), referenceImage.getWidth(), referenceImage.getHeight()))

    def getShiftedPixmap(self, xShift, yShift):
        return ndimage.interpolation.shift(self.px, (int(xShift), int(yShift)), mode='nearest')

    def accumulate(self, addImg, compensateShift=False, roiX0=None, roiY0=None, roiX1=None, roiY1=None):
        if (compensateShift == True) and (self.n_accumulations > 0):
            shift = (0, 0)

            if (roiX0 is None) or (roiY0 is None) or (roiX1 is None) or (roiY1 is None):
                shift = self.calcRelativeShift(addImg)
            else:
                # Crop image to drift ROI,
                croppedRef = copy.deepcopy(self)
                croppedRef.crop(x0=roiX0, y0=roiY0, x1=roiX1, y1=roiY1)

                croppedImg = copy.deepcopy(addImg)
                croppedImg.crop(x0=roiX0, y0=roiY0, x1=roiX1, y1=roiY1)

                shift = croppedImg.calcRelativeShift(croppedRef)

            log("Shift: {}".format(shift))
            shiftedPixMap = addImg.getShiftedPixmap(shift[1], shift[0])
            addImg.setPixelMap(shiftedPixMap)

        if self.n_accumulations == 0:
            self.setPixelMap(addImg.getPixelMap())
        else:
            if (self.dimensionsMatch(addImg)):
                self.px += addImg.getPixelMap()
            else:
                raise Exception("Current pixel dimensions ({currentX}x{currentY}) don't match dimensions of new file ({newX}x{newY}): {filename}".format(currentX=self.getWidth(), currentY=self.getHeight(), newX=addImg.getWidth(), newY=addImg.getHeight(), filename=addImg.inputFile.getFilename()))

        self.n_accumulations += 1

    def resetAccumulations(self):
        self.n_accumulations = 0

    def averageAccumulations(self):
        if self.n_accumulations > 1:
            self.px = self.px / self.n_accumulations
            log("Accumulated and averaged {} images.".format(self.n_accumulations))
            self.n_accumulations = 1

    def applyDark(self, dark):
        """ Apply dark image correction (offset). """
        if self.dimensionsMatch(dark):
            self.px = self.px - dark.getPixelMap()
        else:
            raise Exception("The dimensions of the image do not match the dimensions of the dark image for offset correction.")

    def applyFlatfield(self, ref, rescaleFactor=1):
        """ Apply flat field correction (free beam white image / gain correction). """
        if self.dimensionsMatch(ref):
            if(not ref.containsPixelValue(0)):  # avoid division by zero
                self.px = (self.px / ref.getPixelMap()) * float(rescaleFactor)
            else: # avoid division by zero
                self.px = (self.px / numpy.clip(ref.getPixelMap(), 0.1, None)) * float(rescaleFactor)
        else:
            raise Exception("The dimensions of the image do not match the dimensions of the flat image for flat field correction.")

    def verticalProfile(self, xPos):
        if xPos < self.getWidth():
            return numpy.ravel(self.px[:,xPos])
        else:
            raise Exception("Requested position for vertical profile is out of bounds: x={} in an image that has {} rows.".format(xPos, self.getWidth()))

    def verticalROIProfile(self, ROI):
        # Take full image if no ROI is given
        if ROI is None:
            ROI = ImageROI(0, 0, self.getWidth(), self.getHeight())

        slc = self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1]

        profile = slc.mean(axis=1)
        return numpy.ravel(profile)

    def horizontalProfile(self, yPos):
        if yPos < self.getHeight():
            return self.px[yPos]
        else:
            raise Exception("Requested position for horizontal profile is out of bounds: y={} in an image that has {} rows.".format(yPos, self.getHeight()))

    def horizontalROIProfile(self, ROI):
        # Take full image if no ROI is given
        if ROI is None:
            ROI = ImageROI(0, 0, self.getWidth(), self.getHeight())

        slc = self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1]

        profile = slc.mean(axis=0)
        return profile

    def pixelsInShape(self, shape, seedPoint=None, mode='center', calculateWeights=False):
        """ Returns all pixels in the given shape (of class Polygon).

            mode:
              'center'   : a pixel's center must be within the shape to be accepted.
              'full'     : all corner points of a pixel must be within the shape to be accepted.
              'partial'  : only one corner point of a pixel must be within the shape to be accepted.

             calculateWeights:
               True      : includes weights in returned pixel coordinate tuples,
               False     : does not include weights in returned pixel coordinate tuples.
        """

        if seedPoint is not None:
            seedX = int(round(seedPoint.x()))
            seedY = int(round(seedPoint.y()))
        else:
            # Start at point p1 of shape:
            seedX = int(shape.points[0].x())
            seedY = int(shape.points[0].y())

        # Make a map of visited pixels. A visited pixel will get value 1:
        visited = numpy.zeros_like(a=self.px, dtype=numpy.dtype('uint8'))

        # Collect all points that belong to the shape in a list:
        contributions = []

        stack = [] # stack of pixels to visit
        stack.append((seedX, seedY))

        # Add seed's neighors to the stack as well:
        for offsetX in [-1, 0, 1]:
            for offsetY in [-1, 0, 1]:
                if not (offsetX==0 and offsetY==0):
                    nx = seedX+offsetX
                    ny = seedY+offsetY
                    stack.append((nx, ny))

        while len(stack) > 0:
            pixel = stack.pop()
            x = pixel[0]
            y = pixel[1]

            if self.contains(x, y):
                if visited[y][x] == 0:
                    visited[y][x] = 1

                    # The pixel coordinate system is shifted by -0.5px against the shape coordinate system. Upper left pixel corner is its coordinate in the shape coordinate system.
                    inside = False

                    # Reserve names but set them up later only when they are needed.
                    center     = None
                    upperLeft  = None
                    upperRight = None
                    lowerLeft  = None
                    lowerRight = None

                    center = Vector(x+0.5, y+0.5, 0)

                    if mode == 'center':
                        inside = shape.is_inside_2D(center)
                    else:
                        upperLeft  = Vector(x,     y,     0)
                        upperRight = Vector(x+1,   y,     0)
                        lowerLeft  = Vector(x,     y+1,   0)
                        lowerRight = Vector(x+1,   y+1,   0)

                        if mode == 'full':
                            inside = shape.is_inside_2D(upperLeft) and shape.is_inside_2D(upperRight) and shape.is_inside_2D(lowerLeft) and shape.is_inside_2D(lowerRight)
                        elif mode == 'partial':
                            inside = True
                            calculateWeights = True

                    if inside:
                        if calculateWeights:
                            # Calculate pixel weight from the area of the clipped pixel:
                            pixelPolygon = Polygon(upperLeft, upperRight, lowerRight, lowerLeft)  # Clockwise order because pixel CS is y-flipped.

                            clippedPixel = pixelPolygon.clip(shape)

                            weight = clippedPixel.area()

                            if weight > 0:
                                contributions.append((x, y, weight))
                            else:
                                continue
                        else:
                            contributions.append((x, y, 0))

                        # Now add neighbors to the stack:
                        for offsetX in [-1, 0, 1]:
                            for offsetY in [-1, 0, 1]:
                                if not (offsetX==0 and offsetY==0):
                                    nx = x+offsetX
                                    ny = y+offsetY
                                    stack.append((nx, ny))

        return contributions

    @staticmethod
    def getPixelWeight(x, y, clipPolygon):
        # Calculate pixel weight from the area of the clipped pixel:
        upperLeft  = Vector(x,   y)
        upperRight = Vector(x+1, y)
        lowerLeft  = Vector(x,   y+1)
        lowerRight = Vector(x+1, y+1)
        pixelPolygon = Polygon(upperLeft, upperRight, lowerRight, lowerLeft)  # Clockwise order because pixel CS is y-flipped.

        clippedPixel = pixelPolygon.clip(clipPolygon)
        weight = clippedPixel.area()

        return weight

    def meanGVinBin_polygonClipping(self, binCenter, sUnit, tUnit, sBoundary, tBoundary, binShape, weightFunction):
        """ Returns all pixels in the bin on the given vector s.

            binCenter:  center of bin in world CS
            s:   unit vector along profile axis
            t:   unit vector along width axis
        """

        roi_x0, roi_y0, roi_x1, roi_y1 = binShape.get_bounding_box()

        # Create a map with pixels' distances to the bin:
        # (measured parallel to s vector):
        roi_height = roi_y1 - roi_y0
        roi_width  = roi_x1 - roi_x0

        roi_xaxis = numpy.linspace(start=roi_x0, stop=roi_x1, num=roi_width+1, endpoint=True, dtype=numpy.dtype('float64'))
        roi_yaxis = numpy.linspace(start=roi_y0, stop=roi_y1, num=roi_height+1, endpoint=True, dtype=numpy.dtype('float64'))

        roi_gridx, roi_gridy = numpy.meshgrid(roi_xaxis, roi_yaxis)

        # Shift by half a pixel, because they must represent
        # pixel centers in shape coordinate system. Also,
        # origin should be the bin center:
        roi_gridx = roi_gridx + 0.5 - binCenter.x()
        roi_gridy = roi_gridy + 0.5 - binCenter.y()

        # Transform coordinates into bin coordinate system (s and t axes):
        bin_grid_dist_s = numpy.abs(roi_gridx*sUnit.x() + roi_gridy*sUnit.y())
        #bin_grid_dist_t = numpy.abs(roi_gridx*tUnit.x() + roi_gridy*tUnit.y())

        # Set those that are too far from bin center in s and t direction to zero:
        bin_grid_dist_s = numpy.where(bin_grid_dist_s < sBoundary, bin_grid_dist_s, 0)
        #bin_grid_dist_t = numpy.where(bin_grid_dist_t < tBoundary, bin_grid_dist_t, 0)
        #bin_grid_dist_mul = bin_grid_dist_s * bin_grid_dist_t
        #pixel_indices = numpy.nonzero(bin_grid_dist_mul)
        pixel_indices = numpy.nonzero(bin_grid_dist_s)
        pixels_x = pixel_indices[1] + roi_x0
        pixels_y = pixel_indices[0] + roi_y0

        weights = weightFunction(pixels_x, pixels_y, binShape)   # vectorized getPixelWeight()

        gvWeighted = self.px[pixels_y,pixels_x] * weights
        weightSum = numpy.sum(weights)
        meanGV = 0
        if weightSum > 0:
            meanGV = numpy.sum(gvWeighted) / weightSum

        return meanGV

    def meanGVinBin(self, binCenter, sUnit, tUnit, sBoundary, tBoundary, binShape, weightFunction):
        """ Returns all pixels in the bin on the given vector s.

            binCenter:  center of bin in world CS
            s:   unit vector along profile axis
            t:   unit vector along width axis
        """

        roi_x0, roi_y0, roi_x1, roi_y1 = binShape.get_bounding_box()

        # Create a map with pixels' distances to the bin:
        # (measured parallel to s vector):
        roi_height = roi_y1 - roi_y0
        roi_width  = roi_x1 - roi_x0

        roi_xaxis = numpy.linspace(start=roi_x0, stop=roi_x1, num=roi_width+1, endpoint=True, dtype=numpy.dtype('float64'))
        roi_yaxis = numpy.linspace(start=roi_y0, stop=roi_y1, num=roi_height+1, endpoint=True, dtype=numpy.dtype('float64'))

        roi_gridx, roi_gridy = numpy.meshgrid(roi_xaxis, roi_yaxis)

        # Shift by half a pixel, because they must represent
        # pixel centers in shape coordinate system. Also,
        # origin should be the bin center:
        roi_gridx = roi_gridx + 0.5 - binCenter.x()
        roi_gridy = roi_gridy + 0.5 - binCenter.y()

        # Transform coordinates into bin coordinate system (s and t axes):
        bin_grid_dist_s = numpy.abs(roi_gridx*sUnit.x() + roi_gridy*sUnit.y())
        #bin_grid_dist_t = numpy.abs(roi_gridx*tUnit.x() + roi_gridy*tUnit.y())

        # Set those that are too far from bin center in s and t direction to zero:
        #bin_grid_dist_s = numpy.where(bin_grid_dist_s < sBoundary, bin_grid_dist_s, 0)
        #bin_grid_dist_t = numpy.where(bin_grid_dist_t < tBoundary, bin_grid_dist_t, 0)
        #bin_grid_dist_mul = bin_grid_dist_s * bin_grid_dist_t
        #pixel_indices = numpy.nonzero(bin_grid_dist_mul)

        pixel_indices = numpy.nonzero(bin_grid_dist_s < sBoundary)
        weights = bin_grid_dist_s[pixel_indices]
        pixels_x = pixel_indices[1] + roi_x0
        pixels_y = pixel_indices[0] + roi_y0

        weights = weightFunction(pixels_x, pixels_y, binShape)   # vectorized getPixelWeight()

        gvWeighted = self.px[pixels_y,pixels_x] * weights
        weightSum = numpy.sum(weights)
        meanGV = 0
        if weightSum > 0:
            meanGV = numpy.sum(gvWeighted) / weightSum

        return meanGV

    """
    def lineProfile_projectPixelsIntoProfileBins(self, x0, y0, x1, y1, width=1, resolution=1):
        # Vector pointing in direction of the requested line:
        s = Vector(x1-x0+1, y1-y0+1, 0)   # +1 to fully include pixel (x1, y1)

        # Calculate vector t, perpendicular to s: t = s x z
        z = Vector(0, 0, 1)
        t = s.cross(z)
        t.make_unit_vector()
        t.scale(0.5*width)

        # Define a rectangle along the line and its width, separated into two triangles.
        origin = Vector(x0, y0, 0)
        A = origin - t
        B = origin + s - t
        C = origin + s + t
        D = origin + t

        rect = Polygon(A, B, C, D)

        print("s: {}".format(s))
        print("t: {}".format(t))

        print(rect)

        ceilLength = math.ceil(s.length())

        nSamples = int( ceilLength / resolution ) + 1   # +1 for endpoint

        # Set a seed point at the center of the rectangle:
        t.scale(0.5)
        s.scale(0.5)
        seed = A + t + s

        # Make a list of unique pixel coordinates within this rectangle:
        pixelsInRect = self.pixelsInShape(shape=rect, seedPoint=seed)

        # Create a histogram:
        sPositions, sStepSize = numpy.linspace(start=0, stop=ceilLength, num=nSamples, endpoint=True, retstep=True)
        sCounts = numpy.zeros_like(a=sPositions, dtype=numpy.dtype('float64'))   # Number of contributions, for correct re-normalization, same datatype for efficiency during division later on...
        sSum = numpy.zeros_like(a=sPositions, dtype=numpy.dtype('float64'))      # The sum of all grey value contributions

        # Make s a unit vector to correctly calculate projections using the dot product:
        s.make_unit_vector()

       # print("shape of positions: {}".format(numpy.shape(sPositions)))

        print("{} pixels in rect.".format(len(pixelsInRect)))

        offset = Vector(0.5, 0.5, 0)

        for pixel in pixelsInRect:
            # Project this pixel onto the s vector (pointing in direction of the line):

            # Move to line origin:
            p = pixel - origin + offset

            # Position on s axis:
            sPos = p.dot(s)

            # Find bin where this grey value should be counted:
            binPos = int(math.floor(sPos / sStepSize))

            #print("({x}, {y}): sPos: {spos}, binPos: {binpos}".format(x=p.x(), y=p.y(), spos=sPos, binpos=binPos))

            sCounts[binPos] += 1
            sSum[binPos] += self.getPixel(int(pixel.x()), int(pixel.y()))

        # Replace zero counts by 1 to avoid div by zero:
        sCounts[sCounts==0] = 1

        sProfile = sSum / sCounts

        return sProfile, sPositions, sStepSize
    """

    def lineProfile(self, x0, y0, x1, y1, width=1, resolution=1):
        """ Find line profile by adding weighted contributions of pixel grey values
            into bins of size (width x resolution).

            We always work in the 'shape coordinate system' with its origin
            at (0, 0) in the upper left corner.
            Center of pixel (0, 0) has shape CS coordinates (0.5, 0.5).

            x0, y0, x1 and y1 are shape coordinates.

            Returned 'sPositions' array contains bin center positions.
            """

        # Vector pointing in direction of the requested line:
        s = Vector(x1-x0, y1-y0, 0)

        # Calculate vector t, perpendicular to s: t = s x z
        z = Vector(0, 0, 1)
        t = s.cross(z)
        t.make_unit_vector()

        # Convert to 2D vectors:
        s = Vector(s.x(), s.y())
        t = Vector(t.x(), t.y())

        tUnit = copy.deepcopy(t)

        t.scale(0.5*width)  # t points from line origin half way in direction of width

        # Define a rectangle along the line and its width.
        origin = Vector(x0, y0)

        nSamples = math.ceil( s.length() / resolution ) #+ 1 # +1 for endpoint
        ceilLength = nSamples * resolution

        # Create a histogram:
        sPositions, sStepSize = numpy.linspace(start=0, stop=ceilLength, num=nSamples, endpoint=False, retstep=True)
        sProfile = numpy.zeros_like(a=sPositions, dtype=numpy.dtype('float64'))   # Grey value profile

        # Create a unit vector in s direction:
        sUnit = copy.deepcopy(s)
        sUnit.make_unit_vector()

        # Half a unit vector:
        binUnitHalf = copy.deepcopy(sUnit)
        binUnitHalf.scale(0.5*resolution)

        # Make s the length of a bin step (i.e. resolution unit)
        s.make_unit_vector()
        s.scale(resolution)

        rectPos = Vector(0, 0)

        # A pixel center can be this far from the binPos (bin center)
        # in s and t direction to still be accepted:
        sBoundary = (resolution/2) + pixelHalfDiagonal
        tBoundary = (width/2) + pixelHalfDiagonal

        # Vectorize the pixel weight function:
        weightFunction = numpy.vectorize(self.getPixelWeight, otypes=[numpy.float64])

        i = 0
        for b in range(nSamples):
            print("\rCalculating line profile... {:.1f}%".format(100.0*i/nSamples), end="")
            i += 1
            # Bin position on s axis:
            sPos = resolution*b

            # Construct a vector to the left point of the bin on the s axis:
            rectPos.set_x(sUnit.x())
            rectPos.set_y(sUnit.y())
            rectPos.scale(sPos)
            rectPos.add(origin)

            binPos = rectPos + binUnitHalf

            # Construct a rectangle that contains the area of this bin:
            A = rectPos - t
            B = rectPos + s - t
            C = rectPos + s + t
            D = rectPos + t

            binRect = Polygon(D, C, B, A)  # Clockwise order because pixel CS is y-flipped.

            # Get all pixels and their relative areas in this bin:
            #pixelsInBin = self.pixelsInShape(shape=binRect, seedPoint=rectPos, mode='partial', calculateWeights=True)

            meanGV = self.meanGVinBin(binCenter=binPos, sUnit=sUnit, tUnit=tUnit, sBoundary=sBoundary, tBoundary=tBoundary, binShape=binRect, weightFunction=weightFunction)

            sProfile[b] = meanGV

        # Shift the sPositions by half a bin size so that they represent bin centers:
        sPositions += 0.5*resolution

        print("\rCalculating line profile... 100%   ")
        return sProfile, sPositions, sStepSize

    def clip(self, lower, upper):
        """ Clip grey values to given boundary interval. """
        self.px = numpy.clip(self.px, lower, upper)

    def crop(self, x0, y0, x1, y1):
        """ Crop to given box (x0, y0)--(x1, y1). """
        if x0 > x1:
            x0,x1 = x1,x0

        if y0 > y1:
            y0,y1 = y1,y0

        if y1 > self.getHeight()  or  x1 > self.getWidth():
            raise Exception("Trying to crop beyond image boundaries.")

        self.boundingBoxX0 += x0
        self.boundingBoxY0 += y0

        self.px = self.px[int(y0):int(y1),int(x0):int(x1)]   # Array has shape [y][x]
        self.width  = int(x1 - x0)
        self.height = int(y1 - y0)

    def cropBorder(self, top=0, bottom=0, left=0, right=0):
        """ Crop away given border around image. """
        x0 = int(left)
        y0 = int(top)
        x1 = self.getWidth() - int(right)
        y1 = self.getHeight() - int(bottom)

        self.crop(x0, y0, x1, y1)

    def cropROIaroundPoint(self, centerX, centerY, roiWidth, roiHeight):
        """ Crop a region of interest, centered around given point. """

        if roiWidth < 0:
            roiWidth = abs(roiWidth)
        if roiHeight < 0:
            roiHeight = abs(roiHeight)
        if roiWidth == 0 or roiHeight == 0:
            raise Exception("The region of interest should not be a square of size 0.")

        x0 = int(math.floor(centerX - roiWidth/2))
        x1 = int(math.ceil(centerX + roiWidth/2))
        y0 = int(math.floor(centerY - roiHeight/2))
        y1 = int(math.ceil(centerY + roiHeight/2))

        if x1<0 or y1<0:
            raise Exception("Right or lower boundary for ROI (x1 or y1) cannot be below zero.")

        if roiWidth>self.getWidth() or roiHeight>self.getHeight():
            raise Exception("Size of the ROI is bigger than the image size. ROI: " + str(roiWidth) + " x " + str(roiHeight) + ". Image: " + str(self.getWidth()) + " x " + str(self.getHeight()))
        if x0 < 0:
            x1 += abs(x0)
            x0 = 0

        if y0 < 0:
            y1 += abs(y0)
            y0 = 0

        if x1 >= self.getWidth():
            x1 = self.getWidth()
            x0 = x1 - roiWidth

        if y1 >= self.getHeight():
            y1 = self.getHeight()
            y0 = y1 - roiHeight

        # These should match roiWidth and roiHeight...
        roiDimX = x1 - x0
        roiDimY = y1 - y0

        self.crop(x0, y0, x1, y1)
        return x0, x1, y0, y1

    def bin(self, binSizeX, binSizeY, operation="mean"):
        """ Decrease image size by merging pixels using specified operation.
            Valid operations: mean, max, min, sum. """

        if binSizeX is None:
            binSizeX = 1

        if binSizeY is None:
            binSizeY = 1

        if (binSizeX > 1) or (binSizeY > 1):
            # Picture dimensions must be integer multiple of binning factor. If not, crop:
            overhangX = math.fmod(int(self.getWidth()), binSizeX)
            overhangY = math.fmod(int(self.getHeight()), binSizeY)
            if (overhangX > 0) or (overhangY > 0):
                #log("Cropping before binning because of nonzero overhang: (" + str(overhangX) + ", " + str(overhangY) + ")")
                self.crop(0, 0, self.getWidth()-int(overhangX), self.getHeight()-int(overhangY))

            newWidth  = self.width // binSizeX
            newHeight = self.height // binSizeY

            # Shift pixel values that need to be binned together into additional axes:
            binshape = (newHeight, binSizeY, newWidth, binSizeX)
            self.px = self.px.reshape(binshape)

            # Perform binning operation along binning axes (axis #3 and #1).
            # These axes will be collapsed to contain only the result
            # of the binning operation.
            if operation == "mean":
                self.px = self.px.mean(axis=(3, 1))
            elif operation == "sum":
                self.px = self.px.sum(axis=(3, 1))
            elif operation == "max":
                self.px = self.px.max(axis=(3, 1))
            elif operation == "min":
                self.px = self.px.min(axis=(3, 1))
            elif operation is None:
                raise Exception("No binning operation specified.")
            else:
                raise Exception("Invalid binning operation: {}.".format(operation))

            self.setWidth(newWidth)
            self.setHeight(newHeight)

            # Resolution assumes isotropic pixels...
            self.resolution *= binSizeX

    def addImage(self, other):
        """ Add pixel values from another image to this image. """
        if self.dimensionsMatch(other):
            self.px = self.px + other.getPixelMap()

    def subtractImage(self, other):
        """ Subtract pixel values of another image from this image. """
        if self.dimensionsMatch(other):
            self.px = self.px - other.getPixelMap()

    def multiplyImage(self, other):
        """ Multiply pixel values from another image to this image. """
        if self.dimensionsMatch(other):
            self.px = self.px * other.getPixelMap()

    def divideImage(self, other):
        """ Multiply pixel values by another image. """
        if self.dimensionsMatch(other):
            self.px = self.px / other.getPixelMap()

    def square(self):
        self.px *= self.px

    def sqrt(self):
        self.px = numpy.sqrt(self.px)

    def add(self, value):
        self.px += value

    def subtract(self, value):
        self.px -= value

    def multiply(self, value):
        self.px *= value

    def divide(self, value):
        """ Divide all pixels values by given scalar value. """
        self.px = self.px / float(value)

    def invert(self, min=0, maximum=65535):
        self.px = maximum - self.px

    def renormalize(self, newMin=0, newMax=1, currentMin=None, currentMax=None, ROI=None):
        """Renormalization of grey values from (currentMin, Max) to (newMin, Max) """

        # Take full image if no ROI is given
        if ROI is None:
            ROI = ImageROI(0, 0, self.getWidth(), self.getHeight())

        slc = self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1]

        if currentMin is None:
            currentMin = slc.min()

        if currentMax is None:
            currentMax = slc.max()

        if(currentMax != currentMin):
            slc = (slc-currentMin)*(newMax-newMin)/(currentMax-currentMin)+newMin
            self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1] = slc
        else:
            slc = slc*0
            self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1] = slc
            #raise Exception("Division by zero upon renormalization: currentMax=currentMin={}".format(currentMax))

    def map_lookup(self, gv, gv_from, gv_to):
        """ Return new grey value for given grey value 'gv'. Helper function for self.map()."""

        if gv in gv_from:
            # Given grey value is defined in 'from' list:
            return gv_to[numpy.where(gv_from==gv)]
        else:
            # Linear interpolation:
            a = 0  # left index of interpolation region
            if len(gv_from) > 2:
                for i in range(len(gv_from)-2):
                    if gv_from[i+1] > gv:
                        break

                    a += 1

            b = a + 1  # right index of interpolation region

            xa = gv_from[a]
            xb = gv_from[b]
            ya = gv_to[a]
            yb = gv_to[b]

            # Slope of linear function:
            m = (yb-ya) / (xb-xa)

            # y axis intersection point ("offset"):
            n = yb - m*xb

            # newly assigned grey value:
            return (m*gv + n)


    def map(self, gv_from, gv_to, bins=1000):
        """ Applies a lookup table (LUT map) to convert image grey values
            according to given assignment tables (two numpy lists).

            gv_from: numpy array of given grey values (in current image)
            gv_to:   numpy array of assigned grey values (for converted image)

            Linear interpolation will take place for gaps in lookup table.
        """

        if len(gv_from) == len(gv_to):
            if len(gv_from) > 1:
                gvMin = self.min()
                gvMax = self.max()

                # Left position of each bin:
                positions, gvStepsize = numpy.linspace(start=gvMin, stop=gvMax, num=bins+1, endpoint=True, dtype=numpy.float64, retstep=True)

                # New grey value for each left position:
                mappingFunction = numpy.vectorize(pyfunc=self.map_lookup, excluded={1, 2})
                newGV = mappingFunction(positions, gv_from, gv_to)

                # Differences in newGV:
                deltaGV = numpy.diff(newGV, n=1)


                # Prepare parameters m (slope) and n (offset) for linear
                # interpolation functions of each bin:
                slopes  = numpy.zeros(bins, dtype=numpy.float64)
                offsets = numpy.zeros(bins, dtype=numpy.float64)

                slopes  = deltaGV / gvStepsize

                #print("newGV:     {}".format(numpy.shape(newGV)))
                #print("slopes:    {}".format(numpy.shape(slopes)))
                #print("positions: {}".format(numpy.shape(positions)))

                offsets = newGV[1:] - slopes*positions[1:]

                inverse_stepsize = 1.0 / gvStepsize

                maxIndices = numpy.full(shape=numpy.shape(self.px), fill_value=bins-1, dtype=numpy.uint32)
                bin_indices = numpy.minimum(maxIndices, numpy.floor((self.px - gvMin) * inverse_stepsize).astype(numpy.uint32))

                m_px = slopes[bin_indices]
                n_px = offsets[bin_indices]

                self.px = m_px*self.px + n_px
            else:
                raise Exception("image.map(): At least two mappings are required in the grey value assignment lists.")
        else:
            raise Exception("image.map(): gv_from must have same length as gv_to.")

    def stats(self, ROI=None):
        """ Image or ROI statistics. Mean, Standard Deviation """

        # Take full image if no ROI is given
        if ROI is None:
            ROI = ImageROI(0, 0, self.getWidth(), self.getHeight())

        slc = self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1]

        mean  = numpy.mean(slc)
        sigma = numpy.std(slc)
        snr   = 0
        if sigma > 0:
            snr = mean / sigma

        return {"mean": mean, "stddev": sigma, "snr": snr, "width": ROI.width(), "height": ROI.height(), "area": ROI.area()}

    def noise(self, sigma):
        """ Add noise to image.

            Gaussian noise:
            sigma: standard deviation (scalar or array that matches image size)
        """

        rng = default_rng()
        self.px += rng.normal(loc=0, scale=sigma, size=numpy.shape(self.px))

    def smooth_gaussian(self, sigma):
        self.px = ndimage.gaussian_filter(input=self.px, sigma=sigma, order=0, )

    def applyMedian(self, kernelSize=1):
        if kernelSize > 1:
            self.px = ndimage.median_filter(self.px, int(kernelSize))

    def applyThreshold(self, threshold, lower=0, upper=65535):
        self.px = numpy.where(self.px > threshold, upper, lower).astype(self.getInternalDataType())

    def renormalizeToMeanAndStdDev(self, mean, stdDev, ROI=None):
        """ Renormalize grey values such that mean=30000, (mean-stdDev)=0, (mean+stdDev)=60000 """

        # Take full image if no ROI is given
        if ROI is None:
            ROI = ImageROI(0, 0, self.getWidth(), self.getHeight())

        self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1] = ((self.px[ROI.y0:ROI.y1, ROI.x0:ROI.x1] - mean)/stdDev)*30000 + 30000

    def edges_sobel(self):
        # Sobel edge detection:
        edgesX = ndimage.sobel(self.px, axis=0, mode='nearest')
        edgesY = ndimage.sobel(self.px, axis=1, mode='nearest')
        return numpy.sqrt(edgesX**2 + edgesY**2)

    def edges_canny(self):
        # The 'feature' package from scikit-image,
        # only needed for Canny edge detection, when used instead of Sobel.
        from skimage.feature import canny   # Canny edge detection

        # Canny edge detection. Needs 'scikit-image' package.  from skimage import feature
        return canny(self.px)

    def filter_edges(self, mode='sobel'):
        if(mode == 'sobel'):
            self.px = self.edges_sobel()
        elif(mode == 'canny'):
            self.px = self.edges_canny()
        else:
            raise Exception("Valid edge detection modes: 'sobel'")

        # Rescale:
        self.px = self.px.astype(self.getInternalDataType())
        #self.thresholding(0)    # black=0, white=65535

    def cleanPatches(self, min_patch_area=None, max_patch_area=None, remove_border_patches=False, aspect_ratio_tolerance=None):
        iterationStructure = ndimage.generate_binary_structure(rank=2, connectivity=2)  # apply to rank=2D array, only nearest neihbours (connectivity=1) or next nearest neighbours as well (connectivity=2)

        labelField, nPatches = ndimage.label(self.px, iterationStructure)
        nCleaned   = 0
        nRemaining = 0
        patchGeometry = []

        if nPatches == 0:
            log("Found no structures")
        else:
            self.erase()

            areaMin = 0
            if(min_patch_area is not None):
                areaMin = min_patch_area

            areaMax = self.getWidth() * self.getHeight()
            if(max_patch_area is not None):
                areaMax = max_patch_area

            areaMin = areaMin / (self.getResolution()**2)
            areaMax = areaMax / (self.getResolution()**2)

            for i in range(1, nPatches+1):
                patchCoordinates = numpy.nonzero(labelField==i)

                # Check patch size:
                nPatchPixels = len(patchCoordinates[0])
                if nPatchPixels < areaMin or nPatchPixels > areaMax:  # Black out areas that are too small or too big for a circle
                    nCleaned += 1
                    continue

                coordinatesX = patchCoordinates[1]
                coordinatesY = patchCoordinates[0]

                left  = numpy.amin(coordinatesX)
                right = numpy.amax(coordinatesX)
                top   = numpy.amin(coordinatesY)
                bottom= numpy.amax(coordinatesY)

                if remove_border_patches:
                    if((left==0) or (top==0) or (right==self.getWidth()-1) or (bottom==self.getHeight()-1)):
                        nCleaned += 1
                        continue

                # An ideal circle should have an aspect ratio of 1:
                if aspect_ratio_tolerance is not None:
                    aspectRatio = 0
                    if(top != bottom):
                        aspectRatio = abs(right-left) / abs(bottom-top)

                    if abs(1-aspectRatio) > aspect_ratio_tolerance:  # This is not a circle
                        nCleaned += 1
                        log("Aspect ratio {ar:.3f} doesn't meet aspect ratio tolerance |1-AR|={tolerance:.3f}".format(ar=aspectRatio, tolerance=aspect_ratio_tolerance))
                        continue

                # Add patch center as its coordinate:
                patchGeometry.append(((right+left)/2.0, (bottom+top)/2.0, right-left, bottom-top))

                self.px[patchCoordinates] = 1
                nRemaining += 1

        return nPatches, nCleaned, nRemaining, patchGeometry

    def fitCircle(self):
        # Linear least squares method by:
        # I. D. Coope,
        # Circle Fitting by Linear and Nonlinear Least Squares,
        # Journal of Optimization Theory and Applications, 1993, Volume 76, Issue 2, pp 381-388
        # https://doi.org/10.1007/BF00939613

        coordinates = numpy.nonzero(self.px)
        circlePixelsX = coordinates[1]
        circlePixelsY = coordinates[0]
        circlePixels1 = numpy.ones_like(circlePixelsX)
        nPoints = len(circlePixelsX)

        # Create the matrix B for the system of linear equations:
        matrixB = numpy.array((circlePixelsX, circlePixelsY, circlePixels1))
        matrixB = matrixB.transpose()

        # linear equation to optimize:
        # matrix B * result = vector d
        d = []
        for i in range(nPoints):
            d.append(circlePixelsX[i]**2 + circlePixelsY[i]**2)

        vectorD = numpy.array(d)

        results, residuals, rank, s = numpy.linalg.lstsq(matrixB, vectorD, rcond=None)

        centerX = (results[0] / 2.0)
        centerY = (results[1] / 2.0)
        radius  = math.sqrt(results[2] + centerX**2 + centerY**2)

        # Calculate deviation statistics:
        differenceSum = 0
        minDifference = 99999
        maxDifference = 0
        for i in range(nPoints):
            diff = abs(radius  -  math.sqrt((centerX - circlePixelsX[i])**2 + (centerY - circlePixelsY[i])**2))
            differenceSum += diff

            if minDifference > diff:
                minDifference = diff

            if maxDifference < diff:
                maxDifference = diff

        meanDifference = differenceSum / nPoints

        return centerX, centerY, radius, meanDifference, minDifference, maxDifference

    def intensityFunction2D(self, x, I0, mu, R, x0):   # Lambert-Beer-Law for ball intensity, to fit.
        radicand = numpy.power(R,2) - numpy.power((x-x0),2)

        # Avoid root of negative numbers
        radicand[radicand < 0] = 0

        # Huge radicands lead to exp()->0, therefore avoid huge exponentiation:
        radicand[radicand > (1400*1400)] = (1400*1400)

        result = I0*numpy.exp(-2.0*mu*numpy.sqrt(radicand))

        return result

    def intensityFunction3D(self, coord, I0, mu, R, x0, y0):   # Lambert-Beer-Law for ball intensity, to fit.
        if len(coord) == 2:
            (x, y) = coord

            radicand = numpy.power(R,2) - numpy.power((x-x0),2) - numpy.power((y-y0),2)

            # Avoid root of negative numbers
            radicand[radicand < 0] = 0

            # Huge radicands lead to exp()->0, therefore avoid huge exponentiation:
            radicand[radicand > (1400*1400)] = (1400*1400)

            result = I0 * numpy.exp(-2.0*mu*numpy.sqrt(radicand))

            return result
        else:
            raise Exception("3D Intensity fit function expects a tuple (x,y) for coordinates.")

    def fitIntensityProfile(self, axis="x", initI0=None, initMu=0.003, initR=250, initX0=None, avgLines=5):
        yData = 0
        xdata = 0
        if initI0 is None:
            initI0 = self.max()   # Hoping that a median has been applied before.

        if axis == "x":
            if initX0 is None:
                initX0 = self.getWidth() / 2

            startLine = int((self.getHeight() / 2) - math.floor(avgLines/2))
            stopLine  = int((self.getHeight() / 2) + math.floor(avgLines/2))

            # Accumulate intensity profile along 'avgLines' lines around the center line:
            yData = numpy.zeros(self.getWidth(), dtype=self.getInternalDataType())
            for l in range(startLine, stopLine+1):
                yData += self.px[l,:]

            xData = numpy.linspace(0, self.getWidth()-1, self.getWidth())

        elif axis == "y":
            if initX0 is None:
                initX0 = self.getHeight() / 2

            startLine = int((self.getWidth() / 2) - math.floor(avgLines/2))
            stopLine  = int((self.getWidth() / 2) + math.floor(avgLines/2))

            # Accumulate intensity profile along 'avgLines' lines around the center line:
            yData = numpy.zeros(self.getHeight(), dtype=self.getInternalDataType())
            for l in range(startLine, stopLine+1):
                yData += self.px[:,l]

            xData = numpy.linspace(0, self.getHeight()-1, self.getHeight())

        else:
            raise Exception("projectionImage::fitIntensityProfile() needs profile direction to be 'x' or 'y'.")

        yData = yData / int(avgLines)   # average intensity profile
        firstGuess = (initI0, initMu, initR, initX0)

        try:
            optimalParameters, covariances = optimize.curve_fit(self.intensityFunction2D, xData, yData, p0=firstGuess)
        except Exception:
            optimalParameters = (None, None, None, None)


        fittedI0 = optimalParameters[0]
        fittedMu = optimalParameters[1]
        fittedR  = optimalParameters[2]
        fittedX0 = optimalParameters[3]

        return fittedI0, fittedMu, fittedR, fittedX0

Static methods

def getPixelWeight(x, y, clipPolygon)

Methods

def accumulate(self, addImg, compensateShift=False, roiX0=None, roiY0=None, roiX1=None, roiY1=None)
def add(self, value)
def addImage(self, other)

Add pixel values from another image to this image.

def applyDark(self, dark)

Apply dark image correction (offset).

def applyFlatfield(self, ref, rescaleFactor=1)

Apply flat field correction (free beam white image / gain correction).

def applyMedian(self, kernelSize=1)
def applyThreshold(self, threshold, lower=0, upper=65535)
def averageAccumulations(self)
def bin(self, binSizeX, binSizeY, operation='mean')

Decrease image size by merging pixels using specified operation. Valid operations: mean, max, min, sum.

def calcRelativeShift(self, referenceImage)
def centerOfMass(self)
def cleanPatches(self, min_patch_area=None, max_patch_area=None, remove_border_patches=False, aspect_ratio_tolerance=None)
def clip(self, lower, upper)

Clip grey values to given boundary interval.

def contains(self, x, y)

Check if (x, y) is within image dimensions.

def containsPixelValue(self, value)

Check if image contains a certain grey value.

def crop(self, x0, y0, x1, y1)

Crop to given box (x0, y0)–(x1, y1).

def cropBorder(self, top=0, bottom=0, left=0, right=0)

Crop away given border around image.

def cropROIaroundPoint(self, centerX, centerY, roiWidth, roiHeight)

Crop a region of interest, centered around given point.

def dimensionsMatch(self, img)

Check if image dimensions match with another image.

def divide(self, value)

Divide all pixels values by given scalar value.

def divideImage(self, other)

Multiply pixel values by another image.

def edges_canny(self)
def edges_sobel(self)
def erase(self, value=0, dataType=None)

Set all pixels to 'value'.

def filter_edges(self, mode='sobel')
def fitCircle(self)
def fitIntensityProfile(self, axis='x', initI0=None, initMu=0.003, initR=250, initX0=None, avgLines=5)
def flip(self, horizontal=False, vertical=False)
def flipHorizontal(self)
def flipVertical(self)
def getBoundingBoxX0(self)
def getBoundingBoxY0(self)
def getDataTypeClippingBoundaries(self, dataType)
def getFileByteOrder(self)
def getHeight(self)
def getHorizontalFlip(self)
def getIndex(self)
def getInternalDataType(self)

Data type used internally for all image data.

def getNPixels(self)

Calculate number of pixels in image.

def getPixel(self, x, y)
def getPixelMap(self)
def getResolution(self)
def getRotation(self)
def getShiftedPixmap(self, xShift, yShift)
def getVerticalFlip(self)
def getWidth(self)
def horizontalProfile(self, yPos)
def horizontalROIProfile(self, ROI)
def intensityFunction2D(self, x, I0, mu, R, x0)
def intensityFunction3D(self, coord, I0, mu, R, x0, y0)
def invert(self, min=0, maximum=65535)
def isSet(self)

Check if image has a valid width and height.

def lineProfile(self, x0, y0, x1, y1, width=1, resolution=1)

Find line profile by adding weighted contributions of pixel grey values into bins of size (width x resolution).

We always work in the 'shape coordinate system' with its origin at (0, 0) in the upper left corner. Center of pixel (0, 0) has shape CS coordinates (0.5, 0.5).

x0, y0, x1 and y1 are shape coordinates.

Returned 'sPositions' array contains bin center positions.

def map(self, gv_from, gv_to, bins=1000)

Applies a lookup table (LUT map) to convert image grey values according to given assignment tables (two numpy lists).

gv_from: numpy array of given grey values (in current image) gv_to: numpy array of assigned grey values (for converted image)

Linear interpolation will take place for gaps in lookup table.

def map_lookup(self, gv, gv_from, gv_to)

Return new grey value for given grey value 'gv'. Helper function for self.map().

def max(self, ROI=None)

Return maximum intensity in image.

def mean(self, ROI=None)

Return arithmetic mean of the image grey values.

def meanGVinBin(self, binCenter, sUnit, tUnit, sBoundary, tBoundary, binShape, weightFunction)

Returns all pixels in the bin on the given vector s.

binCenter: center of bin in world CS s: unit vector along profile axis t: unit vector along width axis

def meanGVinBin_polygonClipping(self, binCenter, sUnit, tUnit, sBoundary, tBoundary, binShape, weightFunction)

Returns all pixels in the bin on the given vector s.

binCenter: center of bin in world CS s: unit vector along profile axis t: unit vector along width axis

def min(self, ROI=None)

Return minimum intensity in image.

def multiply(self, value)
def multiplyImage(self, other)

Multiply pixel values from another image to this image.

def noise(self, sigma)

Add noise to image.

Gaussian noise: sigma: standard deviation (scalar or array that matches image size)

def pixelsInShape(self, shape, seedPoint=None, mode='center', calculateWeights=False)

Returns all pixels in the given shape (of class Polygon).

mode: 'center' : a pixel's center must be within the shape to be accepted. 'full' : all corner points of a pixel must be within the shape to be accepted. 'partial' : only one corner point of a pixel must be within the shape to be accepted.

calculateWeights: True : includes weights in returned pixel coordinate tuples, False : does not include weights in returned pixel coordinate tuples.

def read(self, filename=None, width=None, height=None, index=0, dataType=None, byteOrder=None, fileHeaderSize=0, imageHeaderSize=0)

Read TIFF or RAW, decide by file name.

def readRAW(self, width, height, index=0, dataType=None, byteOrder=None, fileHeaderSize=0, imageHeaderSize=0)

Import RAW image file.

def readTIFF(self, flipByteOrder=False, obeyOrientation=True)

Import TIFF file.

def renormalize(self, newMin=0, newMax=1, currentMin=None, currentMax=None, ROI=None)

Renormalization of grey values from (currentMin, Max) to (newMin, Max)

def renormalizeToMeanAndStdDev(self, mean, stdDev, ROI=None)

Renormalize grey values such that mean=30000, (mean-stdDev)=0, (mean+stdDev)=60000

def resetAccumulations(self)
def rot180(self)
def rot270(self)
def rot90(self)
def rotate(self, rotation)
def save(self, filename=None, dataType=None, byteOrder=None, appendChunk=False, clipValues=True)

Save image as TIFF or RAW.

def saveRAW(self, filename=None, dataType=None, byteOrder=None, appendChunk=False, clipValues=True, addInfo=False)
def saveTIFF(self, filename=None, dataType=None, clipValues=True)
def setFlip(self, horz=False, vert=False)
def setHeight(self, height)

Set image height in px.

def setIndex(self, index)

Set image index position in 3D stack (in px).

def setInputFile(self, inputFile)

Set input file properties from ImageFile object or string.

def setOutputFile(self, outputFile)

Set output file properties from ImageFile object or string.

def setPixel(self, x, y, value)
def setPixelMap(self, px)
def setRotation(self, rotation)
def setWidth(self, width)

Set image width in px.

def shape(self, width, height, index=0, dataType=None, value=0)

Re-format image to given dimensions and data type.

def shapeLike(self, otherImg, dataType=None)
def smooth_gaussian(self, sigma)
def sqrt(self)
def square(self)
def stats(self, ROI=None)

Image or ROI statistics. Mean, Standard Deviation

def stdDev(self, ROI=None)

Return the standard deviation of the image grey values.

def subtract(self, value)
def subtractImage(self, other)

Subtract pixel values of another image from this image.

def verticalProfile(self, xPos)
def verticalROIProfile(self, ROI)
class ImageFile (filename=None, dataType=None, byteOrder=None, flipByteOrder=False)

Fundamental image file properties used for input and output.

Expand source code
class ImageFile:
    """Fundamental image file properties used for input and output."""

    def __init__(self, filename=None, dataType=None, byteOrder=None, flipByteOrder=False):
        self.filename  = None
        self.dataType  = None
        self.byteOrder = None   # 'little' or 'big' endian
        self.flipByteOrder = False

        self.setFilename(filename)
        self.setDataType(dataType)
        self.setByteOrder(byteOrder)
        self.setFlipByteOrder(flipByteOrder)

    def setFilename(self, filename):
        self.filename = filename

    def getFilename(self) -> str:
        return self.filename

    def getFileBasename(self) -> str:
        return os.path.basename(self.filename)

    def getDataType(self) -> str:
        return self.dataType

    def getByteOrder(self) -> str:
        return self.byteOrder

    def doFlipByteOrder(self) -> bool:
        return self.flipByteOrder

    def setDataType(self, dataType: str):
        """ Set data type, either from numpy.dtype object or string. """
        if isinstance(dataType, numpy.dtype):
            self.dataType = dataType
        elif dataType is None:
            self.dataType = None
        elif isinstance(dataType, str):  # from string
            dt = numpy.dtype(dataType)
            self.setDataType(dt)
        else:
            raise Exception("{} is generally not a valid data type.".format(dataType))

    def setByteOrder(self, byteOrder: str):
        """ Set endianness, do sanity check before. """
        if byteOrder=='little' or byteOrder=='big' or byteOrder is None:
            self.byteOrder = byteOrder
        else:
            raise Exception("{} is not a valid byte order. Must be 'little' or 'big'.".format(byteOrder))

    def setFlipByteOrder(self, flipByteOrder: bool):
        self.flipByteOrder = flipByteOrder

    def isInt(self) -> bool:
        """ True if data type is supported int data type. """
        return numpy.issubdtype(self.dataType, numpy.integer)

    def isFloat(self) -> bool:
        """ True if data type is supported float data type. """
        return numpy.issubdtype(self.dataType, numpy.floating)

Methods

def doFlipByteOrder(self) ‑> bool
def getByteOrder(self) ‑> str
def getDataType(self) ‑> str
def getFileBasename(self) ‑> str
def getFilename(self) ‑> str
def isFloat(self) ‑> bool

True if data type is supported float data type.

def isInt(self) ‑> bool

True if data type is supported int data type.

def setByteOrder(self, byteOrder: str)

Set endianness, do sanity check before.

def setDataType(self, dataType: str)

Set data type, either from numpy.dtype object or string.

def setFilename(self, filename)
def setFlipByteOrder(self, flipByteOrder: bool)
class ImageROI (x0, y0, x1, y1)

Defines a region of interest: upper left and lower right corner.

Expand source code
class ImageROI:
    """ Defines a region of interest: upper left and lower right corner. """

    def __init__(self, x0, y0, x1, y1):
        self.x0 = 0
        self.y0 = 0
        self.x1 = 0
        self.y1 = 0
        self.set(x0, y0, x1, y1)

    def __str__(self):
        return "({x0}, {y0}) -- ({x1}, {y1})".format(x0=self.x0, y0=self.y0, x1=self.x1, y1=self.y1)

    def set(self, x0, y0, x1, y1):
        if x1 < x0:
            x0, x1 = x1, x0

        if y1 < y0:
            y0, y1 = y1, y0

        self.x0 = int(x0)
        self.y0 = int(y0)
        self.x1 = int(x1)
        self.y1 = int(y1)

    def width(self):
        return self.x1 - self.x0

    def height(self):
        return self.y1 - self.y0

    def area(self):
        return self.width()*self.height()

    def grow(self, amount):
        amount = int(amount)
        self.set(self.x0-amount, self.y0-amount, self.x1+amount, self.y1+amount)

Methods

def area(self)
def grow(self, amount)
def height(self)
def set(self, x0, y0, x1, y1)
def width(self)
class ImageStack (filePattern: str = None, width: int = None, height: int = None, dataType: str = None, byteOrder: str = None, rawFileHeaderSize: int = 0, rawImageHeaderSize: int = 0, slices: int = None, startNumber: int = 0, flipByteOrder: bool = False)

Specify an image stack from a single file (RAW chunk) or a collection of single 2D RAW or TIFF files.

The constructor takes the following optional parameters.

Parameters

filePattern : str
Name of a single file or a numbered file stack. If file names end on .tif or .tiff, those are assumed to be TIFF files.
width : int
Image width (in pixels) of a raw file or chunk. Required for RAW input.
height : int
Image height (in pixels) of a raw file or chunk. Required for RAW input.
dataType : str

Data type of the gray value representation. Required for RAW input and output and TIFF output.

Options are: "int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64", "float32", and "float64".

byteOrder : str

Byte order (endianness) of the raw data.

Default value: system default.

Options: "little", "big", None (system default)

rawFileHeaderSize : int

File header size (in bytes) of an input RAW file.

Default value: 0

rawImageHeaderSize : int

Image header size (in bytes) for each image in an input RAW chunk that contains several image slices.

Default value: 0

slices : int

Number of image slices to be read from a given input RAW file, or number of image files to be read from a given image sequence.

Default value: the number of slices will be determined automatically from the RAW chunk file size or from the number of images in the provided sequence.

startNumber : int
For image sequences. The index in the sequential number in the input filename where to start reading files for this stack.
flipByteOrder : bool
Only for TIFF input. The byte order (big or little endian) should be determined by the TIFF loader and be imported correctly. If not, you can use this parameter to flip the byte order after reading a TIFF file.
Expand source code
class ImageStack:
    """ Specify an image stack from a single file (RAW chunk) or
        a collection of single 2D RAW or TIFF files. """

    def __init__(self, filePattern:str=None, width:int=None, height:int=None, dataType:str=None, byteOrder:str=None, rawFileHeaderSize:int=0, rawImageHeaderSize:int=0, slices:int=None, startNumber:int=0, flipByteOrder:bool=False):
        """The constructor takes the following optional parameters.

        Parameters
        ----------
        filePattern : str
            Name of a single file or a numbered file stack.
            If file names end on `.tif` or `.tiff`, those are assumed to be TIFF files.

        width : int
            Image width (in pixels) of a raw file or chunk.
            Required for RAW input.

        height : int
            Image height (in pixels) of a raw file or chunk.
            Required for RAW input.

        dataType : str
            Data type of the gray value representation.
            Required for RAW input and output and TIFF output.

            Options are: `"int8"`, `"int16"`, `"int32"`, `"int64"`,
            `"uint8"`, `"uint16"`, `"uint32"`, `"uint64"`, `"float32"`, and `"float64"`.

        byteOrder : str
            Byte order (endianness) of the raw data.

            Default value: system default.

            Options: `"little"`, `"big"`, `None` (system default)

        rawFileHeaderSize : int
            File header size (in bytes) of an input RAW file.

            Default value: `0`

        rawImageHeaderSize : int
            Image header size (in bytes) for each image in an input RAW chunk that contains several image slices.

            Default value: `0`

        slices : int
            Number of image slices to be read from a given input RAW file, or number of image files to be read from a given image sequence.

            Default value: the number of slices will be determined automatically from the RAW chunk file size or from the number of images in the provided sequence.

        startNumber : int
            For image sequences. The index in the sequential number in the input filename where to start reading files for this stack.

        flipByteOrder : bool
            Only for TIFF input. The byte order (big or little endian) should be determined by the TIFF loader and be imported correctly. If not, you can use this parameter to flip the byte order after reading a TIFF file.
        """

        self.files = ImageFile(filePattern, dataType, byteOrder, flipByteOrder)

        # Has this stack already been built?
        self.built = False

        self.width       = width
        self.height      = height
        self.nSlices     = slices   # number of slices in stack
        self.startNumber = startNumber

        # A RAW chunk can contain an overall file header, and
        # each image in the stack can contain an image header.
        self.rawFileHeaderSize = rawFileHeaderSize
        self.rawImageHeaderSize = rawImageHeaderSize

        self._isVolumeChunk = False    # Is this a volume chunk or is a file list provided?

        self.fileList = []
        self.fileNumbers = []   # store original stack number in file name

    def addStack(self, other):
        if (self.width == other.width) and (self.height == other.height):
            self.nSlices += other.nSlices
            self.fileList.extend(other.fileList)
            self.fileNumbers.extend(other.fileNumbers)
        else:
            raise Exception("Error adding stack: image dimensions don't match.")

    def isVolumeChunk(self):
        return self._isVolumeChunk

    def setVolumeChunk(self, isVolumeChunk):
        self._isVolumeChunk = isVolumeChunk

    def getFileByteOrder(self):
        return self.files.getByteOrder()

    def setFileByteOrder(self, byteOrder):
        self.files.setByteOrder(byteOrder)

    def getFileDataType(self):
        return self.files.getDataType()

    def setFileDataType(self, dataType):
        self.files.setDataType(dataType)

    def doFlipByteOrder(self):
        return self.files.doFlipByteOrder()

    def setFlipByteOrder(self, flipByteOrder):
        self.files.setFlipByteOrder(flipByteOrder)

    def fileStackInfo(self, filenameString):
        """ Split file pattern into lead & trail text, number of expected digits. """
        if '%' in filenameString:
            # A % sign in the provided file pattern indicates an image stack: e.g. %04d
            percentagePosition = filenameString.find("%")

            numberStart = percentagePosition + 1
            numberStop  = filenameString.find("d", percentagePosition)

            leadText  = ""
            if(percentagePosition > 0):
                leadText = filenameString[:percentagePosition]

            trailText = ""
            if((numberStop+1) < len(filenameString)):
                trailText = filenameString[(numberStop+1):]

            if(numberStop > numberStart):
                numberString = filenameString[numberStart:numberStop]
                if(numberString.isdigit()):
                    nDigitsExpected = int(numberString)
                    return leadText, trailText, nDigitsExpected
                else:
                    raise Exception("Image stack pattern is wrong. The wildcard for sequential digits in a filename must be %, followed by number of digits, followed by d, e.g. %04d")
            else:
                raise Exception("Image stack pattern is wrong. The wildcard for sequential digits in a filename must be %, followed by number of digits, followed by d, e.g. %04d")

        return filenameString, "", 0

    def buildStack(self):
        """ Build list of files that match given file name pattern. """
        self.fileList = []
        self.fileNumbers = []

        # Treat projection files
        inFilePattern = self.files.getFilename()
        inputFolder  = os.path.dirname(inFilePattern)
        projBasename = os.path.basename(inFilePattern)

        if inputFolder == "" or inputFolder is None:
            inputFolder = "."

        # Check if an image stack is provided:
        if('%' not in inFilePattern):
            self.fileList.append(inFilePattern)

            if(isTIFF(inFilePattern)):  # treat as single TIFF projection
                self._isVolumeChunk = False
                testImage = Image(inFilePattern)
                testImage.read()
                self.width    = testImage.getWidth()
                self.height   = testImage.getHeight()
                self.nSlices  = 1
                self.files.setDataType(testImage.inputFile.getDataType())
            else:  # treat as raw chunk
                if (self.width is not None) and (self.height is not None):
                    if (self.files.getDataType() is not None):
                        if os.path.isfile(inFilePattern):
                            self._isVolumeChunk = True

                            if (self.nSlices is None):
                                # Determine number of slices.
                                fileSizeInBytes = os.path.getsize(inFilePattern)
                                dataSizeInBytes = fileSizeInBytes - self.rawFileHeaderSize
                                bytesPerImage = self.rawImageHeaderSize + self.width * self.height * self.files.getDataType().itemsize

                                if (dataSizeInBytes >= bytesPerImage):
                                    if (dataSizeInBytes % bytesPerImage) == 0:
                                        self.nSlices = int(dataSizeInBytes / bytesPerImage)
                                        log("{} slices found in raw chunk.".format(self.nSlices))
                                    else:
                                        raise Exception("The raw chunk data size ({} bytes, without general file header) is not divisible by the calculated size of a single image ({} bytes, including image header). Therefore, the number of slices cannot be determined. {}".format(dataSizeInBytes, bytesPerImage, inFilePattern))
                                else:
                                    raise Exception("The raw chunk data size ({} bytes, without general file header) is smaller than the calculated size of a single image ({} bytes, including image header). {}".format(dataSizeInBytes, bytesPerImage, inFilePattern))
                        else:
                            raise Exception("File not found: {}".format(inFilePattern))
                    else:
                        raise Exception("Please provide the data type of the raw chunk.")
                else:
                    raise Exception("Please provide width and height (in pixels) of the raw chunk.")
        else:
            # A % sign in the provided file pattern indicates an image stack: e.g. %04d
            leadText, trailText, nDigitsExpected = self.fileStackInfo(projBasename)

            # Get list of files in input folder:
            fileList = os.listdir(inputFolder)
            fileList.sort()

            nImported = 0

            for f in fileList:
                file = inputFolder + "/" + f
                if os.path.isfile(file):
                    # Check if filename matches pattern:
                    if(f.startswith(leadText) and f.endswith(trailText)):
                        digitText = f[len(leadText):-len(trailText)]
                        if digitText.isdigit(): # and len(digitText)==nDigitsExpected:
                            # Pattern matches.
                            n = int(digitText)
                            if n >= self.startNumber:
                                self.fileList.append(file)
                                self.fileNumbers.append(n)

                                nImported += 1
                                if nImported == self.nSlices:
                                    break
                        else:
                            continue
                    else:
                        continue

            self.nSlices = len(self.fileList)

            if self.nSlices > 0:
                if isTIFF(self.fileList[0]):
                    testImage = Image(self.fileList[0])
                    testImage.read()
                    self.width    = testImage.getWidth()
                    self.height   = testImage.getHeight()
                    self.files.setDataType(testImage.inputFile.getDataType())

        self.built = True


    def getFilename(self, index=None):
        if index is not None:
            if self._isVolumeChunk:
                if len(self.fileList) > 0:
                    return self.fileList[0]
                else:
                    return None
            else:
                if len(self.fileList) > index:
                    return self.fileList[index]
                else:
                    return None
        else:
            return self.files.getFilename()

    def getFileBasename(self, index=None):
        if index is not None:
            if self._isVolumeChunk:
                if len(self.fileList) > 0:
                    return os.path.basename(self.fileList[0])
                else:
                    return None
            else:
                if len(self.fileList) > index:
                    return os.path.basename(self.fileList[index])
                else:
                    return None
        else:
            return self.files.getFileBasename()

    def setFilename(self, filename):
        self.files.setFilename(filename)

    def getImage(self, index, outputFile=None):
        """ Read and return image at position 'index' within the stack. """
        if index >= 0:
            if not self._isVolumeChunk:  # read single image file from stack:
                if len(self.fileList) > index:
                    filename = self.fileList[index]
                    file = ImageFile(filename=filename, dataType=self.getFileDataType(), byteOrder=self.getFileByteOrder(), flipByteOrder=self.doFlipByteOrder())

                    img = Image(file, outputFile)
                    if isTIFF(filename):
                        img.read()
                    else:
                        img.readRAW(self.width, self.height, 0, self.getFileDataType(), self.getFileByteOrder(), self.rawFileHeaderSize, self.rawImageHeaderSize)
                    return img
                else:
                    raise Exception("The requested slice nr. {} is out of bounds, because only {} image files were found.".format(index, len(self.fileList)))
            else:  # read slice from volume chunk, obeying start number
                if len(self.fileList) > 0:
                    file = self.fileList[0]
                    img = Image(file, outputFile)
                    chunkIndex = index + self.startNumber
                    if isTIFF(file):
                        raise Exception("Cannot treat 3D TIFFs.")
                    else:
                        img.readRAW(self.width, self.height, chunkIndex, self.getFileDataType(), self.getFileByteOrder(), self.rawFileHeaderSize, self.rawImageHeaderSize)
                        return img
                else:
                    raise Exception("No image file specified to be loaded.")
        else:
            raise Exception("Negative slice numbers do not exists. {} requested.".format(index))

    def getMeanImage(self, outputFile=None):
        """ Calculate the mean of all image files. """
        if self.nSlices > 0:
            if self.nSlices > 1:
                sumImg = self.getImage(0, outputFile)
                for i in range(1, self.nSlices):
                    print("\rMean Image: summing up {i}/{n}".format(i=(i+1), n=self.nSlices), end='')
                    sumImg.addImage(self.getImage(i, outputFile))


                print("")

                sumImg.divide(self.nSlices)
                return sumImg
            else:
                return self.getImage(0, outputFile)
        else:
            return None

    def getStdDevImage(self, meanImg=None, outputFile=None):
        """ Calculate the pixel-wise RMS of the image files. """
        if self.nSlices > 0:
            if self.nSlices > 1:
                if meanImg is None:
                    meanImg = self.getMeanImage(outputFile)

                sumImg = Image()
                sumImg.shapeLike(otherImg=meanImg)

                for i in range(0, self.nSlices):
                    print("\rRMSD Image: component {i}/{n}".format(i=i+1, n=self.nSlices), end='')
                    sqDiffImg = self.getImage(i, outputFile)
                    sqDiffImg.subtractImage(meanImg)
                    sqDiffImg.square()

                    sumImg.addImage(sqDiffImg)

                sumImg.divide(self.nSlices)
                sumImg.sqrt()

                print("")

                return sumImg
            else:
                return self.getImage(0, outputFile)
        else:
            return None

Methods

def addStack(self, other)
def buildStack(self)

Build list of files that match given file name pattern.

def doFlipByteOrder(self)
def fileStackInfo(self, filenameString)

Split file pattern into lead & trail text, number of expected digits.

def getFileBasename(self, index=None)
def getFileByteOrder(self)
def getFileDataType(self)
def getFilename(self, index=None)
def getImage(self, index, outputFile=None)

Read and return image at position 'index' within the stack.

def getMeanImage(self, outputFile=None)

Calculate the mean of all image files.

def getStdDevImage(self, meanImg=None, outputFile=None)

Calculate the pixel-wise RMS of the image files.

def isVolumeChunk(self)
def setFileByteOrder(self, byteOrder)
def setFileDataType(self, dataType)
def setFilename(self, filename)
def setFlipByteOrder(self, flipByteOrder)
def setVolumeChunk(self, isVolumeChunk)