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 theImage.read()
andImage.save()
routines.ImageStack
represents a stack of images in the file system. It can be used in combination with a processing pipeline (seectsimu.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")
Expand source code
# -*- coding: UTF-8 -*-
"""
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")
"""
import numpy
import os # File and path handling
import sys # To get native byte order ('little' or 'big' endian?)
import math
import copy
from numpy.random import default_rng
# Scipy:
# 'ndimage' class for image processing
# 'optimize' class for intensity fit
# 'signal' class for drift analysis using FFT Convolution
from scipy import ndimage, optimize, stats, signal, fft
from .helpers import *
from .primitives import * # Vectors and Polygons
from .tiffy import tiff
# pixelHalfDiagonal: longest distance a pixel center can have from a line
# while still touching the line with a corner point:
pixelHalfDiagonal = 1.0/math.sqrt(2.0)
def isTIFF(filename: str) -> bool:
"""Check if file name signifies a TIFF image."""
if filename is not None:
if(filename.casefold().endswith('.tif') or filename.casefold().endswith('.tiff')):
return True
return False
def createImageStack(stack):
""" Return an ImageStack object, if string is given. """
if isinstance(stack, ImageStack):
return stack
elif isinstance(stack, str):
return ImageStack(stack)
elif stack is None:
return None
else:
raise Exception("Not a valid image file stack definition: {}".format(stack))
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)
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)
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
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
Functions
def createImageStack(stack)
-
Return an ImageStack object, if string is given.
Expand source code
def createImageStack(stack): """ Return an ImageStack object, if string is given. """ if isinstance(stack, ImageStack): return stack elif isinstance(stack, str): return ImageStack(stack) elif stack is None: return None else: raise Exception("Not a valid image file stack definition: {}".format(stack))
def isTIFF(filename: str) ‑> bool
-
Check if file name signifies a TIFF image.
Expand source code
def isTIFF(filename: str) -> bool: """Check if file name signifies a TIFF image.""" if filename is not None: if(filename.casefold().endswith('.tif') or filename.casefold().endswith('.tiff')): return True return False
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)
-
Expand source code
@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
Methods
def accumulate(self, addImg, compensateShift=False, roiX0=None, roiY0=None, roiX1=None, roiY1=None)
-
Expand source code
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 add(self, value)
-
Expand source code
def add(self, value): self.px += value
def addImage(self, other)
-
Add pixel values from another image to this image.
Expand source code
def addImage(self, other): """ Add pixel values from another image to this image. """ if self.dimensionsMatch(other): self.px = self.px + other.getPixelMap()
def applyDark(self, dark)
-
Apply dark image correction (offset).
Expand source code
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).
Expand source code
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 applyMedian(self, kernelSize=1)
-
Expand source code
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)
-
Expand source code
def applyThreshold(self, threshold, lower=0, upper=65535): self.px = numpy.where(self.px > threshold, upper, lower).astype(self.getInternalDataType())
def averageAccumulations(self)
-
Expand source code
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 bin(self, binSizeX, binSizeY, operation='mean')
-
Decrease image size by merging pixels using specified operation. Valid operations: mean, max, min, sum.
Expand source code
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 calcRelativeShift(self, referenceImage)
-
Expand source code
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 centerOfMass(self)
-
Expand source code
def centerOfMass(self): return ndimage.center_of_mass(self.px)
def cleanPatches(self, min_patch_area=None, max_patch_area=None, remove_border_patches=False, aspect_ratio_tolerance=None)
-
Expand source code
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 clip(self, lower, upper)
-
Clip grey values to given boundary interval.
Expand source code
def clip(self, lower, upper): """ Clip grey values to given boundary interval. """ self.px = numpy.clip(self.px, lower, upper)
def contains(self, x, y)
-
Check if (x, y) is within image dimensions.
Expand source code
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 containsPixelValue(self, value)
-
Check if image contains a certain grey value.
Expand source code
def containsPixelValue(self, value): """ Check if image contains a certain grey value. """ return numpy.any(self.px == value)
def crop(self, x0, y0, x1, y1)
-
Crop to given box (x0, y0)–(x1, y1).
Expand source code
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.
Expand source code
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.
Expand source code
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 dimensionsMatch(self, img)
-
Check if image dimensions match with another image.
Expand source code
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 divide(self, value)
-
Divide all pixels values by given scalar value.
Expand source code
def divide(self, value): """ Divide all pixels values by given scalar value. """ self.px = self.px / float(value)
def divideImage(self, other)
-
Multiply pixel values by another image.
Expand source code
def divideImage(self, other): """ Multiply pixel values by another image. """ if self.dimensionsMatch(other): self.px = self.px / other.getPixelMap()
def edges_canny(self)
-
Expand source code
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 edges_sobel(self)
-
Expand source code
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 erase(self, value=0, dataType=None)
-
Set all pixels to 'value'.
Expand source code
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 filter_edges(self, mode='sobel')
-
Expand source code
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 fitCircle(self)
-
Expand source code
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 fitIntensityProfile(self, axis='x', initI0=None, initMu=0.003, initR=250, initX0=None, avgLines=5)
-
Expand source code
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
def flip(self, horizontal=False, vertical=False)
-
Expand source code
def flip(self, horizontal=False, vertical=False): if horizontal: self.flipHorizontal() if vertical: self.flipVertical()
def flipHorizontal(self)
-
Expand source code
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)
-
Expand source code
def flipVertical(self): self.flipVert = not self.flipVert if self.isSet(): self.px = numpy.require(numpy.flipud(self.px), requirements=['C_CONTIGUOUS'])
def getBoundingBoxX0(self)
-
Expand source code
def getBoundingBoxX0(self): return self.boundingBoxX0
def getBoundingBoxY0(self)
-
Expand source code
def getBoundingBoxY0(self): return self.boundingBoxY0
def getDataTypeClippingBoundaries(self, dataType)
-
Expand source code
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 getFileByteOrder(self)
-
Expand source code
def getFileByteOrder(self): return self.fileByteOrder
def getHeight(self)
-
Expand source code
def getHeight(self): return self.height
def getHorizontalFlip(self)
-
Expand source code
def getHorizontalFlip(self): return self.flipHorz
def getIndex(self)
-
Expand source code
def getIndex(self): return self.index
def getInternalDataType(self)
-
Data type used internally for all image data.
Expand source code
def getInternalDataType(self): """ Data type used internally for all image data. """ return numpy.dtype('float64')
def getNPixels(self)
-
Calculate number of pixels in image.
Expand source code
def getNPixels(self): """ Calculate number of pixels in image. """ return (self.getWidth() * self.getHeight())
def getPixel(self, x, y)
-
Expand source code
def getPixel(self, x, y): return self.px[y][x]
def getPixelMap(self)
-
Expand source code
def getPixelMap(self): return self.px
def getResolution(self)
-
Expand source code
def getResolution(self): return self.resolution
def getRotation(self)
-
Expand source code
def getRotation(self): return self.rotation
def getShiftedPixmap(self, xShift, yShift)
-
Expand source code
def getShiftedPixmap(self, xShift, yShift): return ndimage.interpolation.shift(self.px, (int(xShift), int(yShift)), mode='nearest')
def getVerticalFlip(self)
-
Expand source code
def getVerticalFlip(self): return self.flipVert
def getWidth(self)
-
Expand source code
def getWidth(self): return self.width
def horizontalProfile(self, yPos)
-
Expand source code
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)
-
Expand source code
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 intensityFunction2D(self, x, I0, mu, R, x0)
-
Expand source code
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)
-
Expand source code
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 invert(self, min=0, maximum=65535)
-
Expand source code
def invert(self, min=0, maximum=65535): self.px = maximum - self.px
def isSet(self)
-
Check if image has a valid width and height.
Expand source code
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 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.
Expand source code
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 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.
Expand source code
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 map_lookup(self, gv, gv_from, gv_to)
-
Return new grey value for given grey value 'gv'. Helper function for self.map().
Expand source code
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 max(self, ROI=None)
-
Return maximum intensity in image.
Expand source code
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 mean(self, ROI=None)
-
Return arithmetic mean of the image grey values.
Expand source code
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 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
Expand source code
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 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
Expand source code
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 min(self, ROI=None)
-
Return minimum intensity in image.
Expand source code
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 multiply(self, value)
-
Expand source code
def multiply(self, value): self.px *= value
def multiplyImage(self, other)
-
Multiply pixel values from another image to this image.
Expand source code
def multiplyImage(self, other): """ Multiply pixel values from another image to this image. """ if self.dimensionsMatch(other): self.px = self.px * other.getPixelMap()
def noise(self, sigma)
-
Add noise to image.
Gaussian noise: sigma: standard deviation (scalar or array that matches image size)
Expand source code
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 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.
Expand source code
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
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.
Expand source code
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 readRAW(self, width, height, index=0, dataType=None, byteOrder=None, fileHeaderSize=0, imageHeaderSize=0)
-
Import RAW image file.
Expand source code
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 readTIFF(self, flipByteOrder=False, obeyOrientation=True)
-
Import TIFF file.
Expand source code
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 renormalize(self, newMin=0, newMax=1, currentMin=None, currentMax=None, ROI=None)
-
Renormalization of grey values from (currentMin, Max) to (newMin, Max)
Expand source code
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 renormalizeToMeanAndStdDev(self, mean, stdDev, ROI=None)
-
Renormalize grey values such that mean=30000, (mean-stdDev)=0, (mean+stdDev)=60000
Expand source code
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 resetAccumulations(self)
-
Expand source code
def resetAccumulations(self): self.n_accumulations = 0
def rot180(self)
-
Expand source code
def rot180(self): if self.isSet(): self.px = numpy.require(numpy.rot90(self.px, k=2), requirements=['C_CONTIGUOUS'])
def rot270(self)
-
Expand source code
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 rot90(self)
-
Expand source code
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 rotate(self, rotation)
-
Expand source code
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 save(self, filename=None, dataType=None, byteOrder=None, appendChunk=False, clipValues=True)
-
Save image as TIFF or RAW.
Expand source code
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 saveRAW(self, filename=None, dataType=None, byteOrder=None, appendChunk=False, clipValues=True, addInfo=False)
-
Expand source code
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 saveTIFF(self, filename=None, dataType=None, clipValues=True)
-
Expand source code
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 setFlip(self, horz=False, vert=False)
-
Expand source code
def setFlip(self, horz=False, vert=False): self.flipHorz = horz self.flipVert = vert
def setHeight(self, height)
-
Set image height in px.
Expand source code
def setHeight(self, height): """ Set image height in px. """ self.height = height
def setIndex(self, index)
-
Set image index position in 3D stack (in px).
Expand source code
def setIndex(self, index): """ Set image index position in 3D stack (in px). """ self.index = index
def setInputFile(self, inputFile)
-
Set input file properties from ImageFile object or string.
Expand source code
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.
Expand source code
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 setPixel(self, x, y, value)
-
Expand source code
def setPixel(self, x, y, value): self.px[y][x] = value
def setPixelMap(self, px)
-
Expand source code
def setPixelMap(self, px): self.px = px
def setRotation(self, rotation)
-
Expand source code
def setRotation(self, rotation): self.rotation = rotation
def setWidth(self, width)
-
Set image width in px.
Expand source code
def setWidth(self, width): """ Set image width in px. """ self.width = width
def shape(self, width, height, index=0, dataType=None, value=0)
-
Re-format image to given dimensions and data type.
Expand source code
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)
-
Expand source code
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 smooth_gaussian(self, sigma)
-
Expand source code
def smooth_gaussian(self, sigma): self.px = ndimage.gaussian_filter(input=self.px, sigma=sigma, order=0, )
def sqrt(self)
-
Expand source code
def sqrt(self): self.px = numpy.sqrt(self.px)
def square(self)
-
Expand source code
def square(self): self.px *= self.px
def stats(self, ROI=None)
-
Image or ROI statistics. Mean, Standard Deviation
Expand source code
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 stdDev(self, ROI=None)
-
Return the standard deviation of the image grey values.
Expand source code
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 subtract(self, value)
-
Expand source code
def subtract(self, value): self.px -= value
def subtractImage(self, other)
-
Subtract pixel values of another image from this image.
Expand source code
def subtractImage(self, other): """ Subtract pixel values of another image from this image. """ if self.dimensionsMatch(other): self.px = self.px - other.getPixelMap()
def verticalProfile(self, xPos)
-
Expand source code
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)
-
Expand source code
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)
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
-
Expand source code
def doFlipByteOrder(self) -> bool: return self.flipByteOrder
def getByteOrder(self) ‑> str
-
Expand source code
def getByteOrder(self) -> str: return self.byteOrder
def getDataType(self) ‑> str
-
Expand source code
def getDataType(self) -> str: return self.dataType
def getFileBasename(self) ‑> str
-
Expand source code
def getFileBasename(self) -> str: return os.path.basename(self.filename)
def getFilename(self) ‑> str
-
Expand source code
def getFilename(self) -> str: return self.filename
def isFloat(self) ‑> bool
-
True if data type is supported float data type.
Expand source code
def isFloat(self) -> bool: """ True if data type is supported float data type. """ return numpy.issubdtype(self.dataType, numpy.floating)
def isInt(self) ‑> bool
-
True if data type is supported int data type.
Expand source code
def isInt(self) -> bool: """ True if data type is supported int data type. """ return numpy.issubdtype(self.dataType, numpy.integer)
def setByteOrder(self, byteOrder: str)
-
Set endianness, do sanity check before.
Expand source code
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 setDataType(self, dataType: str)
-
Set data type, either from numpy.dtype object or string.
Expand source code
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 setFilename(self, filename)
-
Expand source code
def setFilename(self, filename): self.filename = filename
def setFlipByteOrder(self, flipByteOrder: bool)
-
Expand source code
def setFlipByteOrder(self, flipByteOrder: bool): self.flipByteOrder = flipByteOrder
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)
-
Expand source code
def area(self): return self.width()*self.height()
def grow(self, amount)
-
Expand source code
def grow(self, amount): amount = int(amount) self.set(self.x0-amount, self.y0-amount, self.x1+amount, self.y1+amount)
def height(self)
-
Expand source code
def height(self): return self.y1 - self.y0
def set(self, x0, y0, x1, y1)
-
Expand source code
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)
-
Expand source code
def width(self): return self.x1 - self.x0
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)
-
Expand source code
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 buildStack(self)
-
Build list of files that match given file name pattern.
Expand source code
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 doFlipByteOrder(self)
-
Expand source code
def doFlipByteOrder(self): return self.files.doFlipByteOrder()
def fileStackInfo(self, filenameString)
-
Split file pattern into lead & trail text, number of expected digits.
Expand source code
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 getFileBasename(self, index=None)
-
Expand source code
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 getFileByteOrder(self)
-
Expand source code
def getFileByteOrder(self): return self.files.getByteOrder()
def getFileDataType(self)
-
Expand source code
def getFileDataType(self): return self.files.getDataType()
def getFilename(self, index=None)
-
Expand source code
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 getImage(self, index, outputFile=None)
-
Read and return image at position 'index' within the stack.
Expand source code
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.
Expand source code
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.
Expand source code
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
def isVolumeChunk(self)
-
Expand source code
def isVolumeChunk(self): return self._isVolumeChunk
def setFileByteOrder(self, byteOrder)
-
Expand source code
def setFileByteOrder(self, byteOrder): self.files.setByteOrder(byteOrder)
def setFileDataType(self, dataType)
-
Expand source code
def setFileDataType(self, dataType): self.files.setDataType(dataType)
def setFilename(self, filename)
-
Expand source code
def setFilename(self, filename): self.files.setFilename(filename)
def setFlipByteOrder(self, flipByteOrder)
-
Expand source code
def setFlipByteOrder(self, flipByteOrder): self.files.setFlipByteOrder(flipByteOrder)
def setVolumeChunk(self, isVolumeChunk)
-
Expand source code
def setVolumeChunk(self, isVolumeChunk): self._isVolumeChunk = isVolumeChunk