Source code for polarTransform.convertToCartesianImage

import concurrent.futures

import scipy.ndimage


[docs]def convertToCartesianImage(image, center=None, initialRadius=None, finalRadius=None, initialAngle=None, finalAngle=None, imageSize=None, hasColor=False, order=3, border='constant', borderVal=0.0, useMultiThreading=False, settings=None): """Convert polar image to cartesian image. Using a polar image, this function creates a cartesian image. This function is versatile because it can automatically calculate an appropriate cartesian image size and center given the polar image. In addition, parameters for converting to the polar domain are necessary for the conversion back to the cartesian domain. Parameters ---------- image : N-dimensional :class:`numpy.ndarray` Polar image to convert to cartesian domain Image should be structured in C-order, i.e. the axes should be ordered (..., z, theta, r, [ch]). The channel axes should only be present if :obj:`hasColor` is :obj:`True`. This format is arbitrary but is selected to stay consistent with the traditional C-order representation in the Cartesian domain. In the mathematical domain, Cartesian coordinates are traditionally represented as (x, y, z) and as (r, theta, z) in the polar domain. When storing Cartesian data in C-order, the axes are usually flipped and the data is saved as (z, y, x). Thus, the polar domain coordinates are also flipped to stay consistent, hence the format (z, theta, r). .. note:: For multi-dimensional images above 2D, the cartesian transformation is applied individually across each 2D slice. The last two dimensions should be the r & theta dimensions, unless :obj:`hasColor` is True in which case the 2nd and 3rd to last dimensions should be. The multidimensional shape will be preserved for the resulting cartesian image (besides the polar dimensions). center : :class:`str` or (2,) :class:`list`, :class:`tuple` or :class:`numpy.ndarray` of :class:`int`, optional Specifies the center in the cartesian image to use as the origin in polar domain. The center in the cartesian domain will be (0, 0) in the polar domain. If center is not set, then it will default to ``middle-middle``. If the image size is :obj:`None`, the center is calculated after the image size is determined. For relative positioning within the image, center can be one of the string values in the table below. The quadrant column contains the visible quadrants for the given center. initialAngle and finalAngle must contain at least one of the quadrants, otherwise an error will be thrown because the resulting cartesian image is blank. An example cartesian image is given below with annotations to what the center will be given a center string. .. table:: Valid center strings :widths: auto ================ =============== ==================== Value Quadrant Location in image ================ =============== ==================== top-left IV 1 top-middle III, IV 2 top-right III 3 middle-left I, IV 4 middle-middle I, II, III, IV 5 middle-right II, III 6 bottom-left I 7 bottom-middle I, II 8 bottom-right II 9 ================ =============== ==================== .. image:: _static/centerAnnotations.png :alt: Center locations for center strings initialRadius : :class:`int`, optional Starting radius in pixels from the center of the cartesian image in the polar image The polar image begins at this radius, i.e. the first row of the polar image corresponds to this starting radius. If initialRadius is not set, then it will default to ``0``. finalRadius : :class:`int`, optional Final radius in pixels from the center of the cartesian image in the polar image The polar image ends at this radius, i.e. the last row of the polar image corresponds to this ending radius. .. note:: The polar image does **not** include this radius. It includes all radii starting from initial to final radii **excluding** the final radius. Rather, it will stop one step size before the final radius. Assuming the radial resolution (see :obj:`radiusSize`) is small enough, this should not matter. If finalRadius is not set, then it will default to the maximum radius which is the size of the radial (1st) dimension of the polar image. initialAngle : :class:`float`, optional Starting angle in radians in the polar image The polar image begins at this angle, i.e. the first column of the polar image corresponds to this starting angle. Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range of 0 to :math:`2\\pi`. If initialAngle is not set, then it will default to ``0.0``. finalAngle : :class:`float`, optional Final angle in radians in the polar image The polar image ends at this angle, i.e. the last column of the polar image corresponds to this ending angle. .. note:: The polar image does **not** include this angle. It includes all angles starting from initial to final angle **excluding** the final angle. Rather, it stops one step size before the final angle. Assuming the angular resolution (see :obj:`angleSize`) is small enough, this should not matter. Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range of 0 to :math:`2\\pi`. If finalAngle is not set, then it will default to :math:`2\\pi`. imageSize : (2,) :class:`list`, :class:`tuple` or :class:`numpy.ndarray` of :class:`int`, optional Desired size of cartesian image where 1st dimension is number of rows and 2nd dimension is number of columns If imageSize is not set, then it defaults to the size required to fit the entire polar image on a cartesian image. hasColor : :class:`bool`, optional Whether or not the polar image contains color channels This means that the image is structured as (..., y, x, ch) or (..., theta, r, ch) for Cartesian or polar images, respectively. If color channels are present, the last dimension (channel axes) will be shifted to the front, converted and then shifted back to its original location. Default is :obj:`False` .. note:: If an alpha band (4th channel of image is present), then it will be converted. Typically, this is unwanted, so the recommended solution is to transform the first 3 channels and set the 4th channel to fully on. order : :class:`int` (0-5), optional The order of the spline interpolation, default is 3. The order has to be in the range 0-5. The following orders have special names: * 0 - nearest neighbor * 1 - bilinear * 3 - bicubic border : {'constant', 'nearest', 'wrap', 'reflect'}, optional Polar points outside the cartesian image boundaries are filled according to the given mode. Default is 'constant' The following table describes the mode and expected output when seeking past the boundaries. The input column is the 1D input array whilst the extended columns on either side of the input array correspond to the expected values for the given mode if one extends past the boundaries. .. table:: Valid border modes and expected output :widths: auto ========== ====== ================= ====== Mode Ext. Input Ext. ========== ====== ================= ====== mirror 4 3 2 1 2 3 4 5 6 7 8 7 6 5 reflect 3 2 1 1 2 3 4 5 6 7 8 8 7 6 nearest 1 1 1 1 2 3 4 5 6 7 8 8 8 8 constant 0 0 0 1 2 3 4 5 6 7 8 0 0 0 wrap 6 7 8 1 2 3 4 5 6 7 8 1 2 3 ========== ====== ================= ====== Refer to :func:`scipy.ndimage.map_coordinates` for more details on this argument. borderVal : same datatype as :obj:`image`, optional Value used for polar points outside the cartesian image boundaries if :obj:`border` = 'constant'. Default is 0.0 useMultiThreading : :class:`bool`, optional Whether to use multithreading when applying transformation for 3D images. This considerably speeds up the execution time for large images but adds overhead for smaller 3D images. Default is :obj:`False` settings : :class:`ImageTransform`, optional Contains metadata for conversion between polar and cartesian image. Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and provides an easy way of passing these parameters along without having to specify them all again. .. warning:: Cleaner and more succint to use :meth:`ImageTransform.convertToCartesianImage` If settings is not specified, then the other arguments are used in this function and the defaults will be calculated if necessary. If settings is given, then the values from settings will be used. Returns ------- cartesianImage : N-dimensional :class:`numpy.ndarray` Cartesian image Resulting image is structured in C-order, i.e. the axes are ordered as (..., z, y, x, [ch]). This format is the traditional method of storing image data in Python. Resulting image shape will be the same as the input image except for the polar dimensions are replaced with the Cartesian dimensions. settings : :class:`ImageTransform` Contains metadata for conversion between polar and cartesian image. Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and provides an easy way of passing these parameters along without having to specify them all again. """ # If there is a color channel present, move it to the front of axes if settings.hasColor if settings is not None else hasColor: image = np.moveaxis(image, -1, 0) # Create settings if none are given if settings is None: # Center is set to middle-middle, which means all four quadrants will be shown if center is None: center = 'middle-middle' # Initial radius of the source image # In other words, what radius does row 0 correspond to? # If not set, default is 0 to get the entire image if initialRadius is None: initialRadius = 0 # Final radius of the source image # In other words, what radius does the last row of polar image correspond to? # If not set, default is the largest radius from image if finalRadius is None: finalRadius = image.shape[-1] # Initial angle of the source image # In other words, what angle does column 0 correspond to? # If not set, default is 0 to get the entire image if initialAngle is None: initialAngle = 0 # Final angle of the source image # In other words, what angle does the last column of polar image correspond to? # If not set, default is 2pi to get the entire image if finalAngle is None: finalAngle = 2 * np.pi # This is used to scale the result of the radius to get the appropriate Cartesian value scaleRadius = image.shape[-1] / (finalRadius - initialRadius) # This is used to scale the result of the angle to get the appropriate Cartesian value scaleAngle = image.shape[-2] / (finalAngle - initialAngle) if imageSize is None: # Obtain the image size by looping from initial to final source angle (every possible theta in the image # basically) thetas = np.mod(np.linspace(0, (finalAngle - initialAngle), image.shape[-2]) + initialAngle, 2 * np.pi) maxRadius = finalRadius * np.ones_like(thetas) # Then get the maximum radius of the image and compute the x/y coordinates for each option # If a center is not specified, then use the origin as a default. This will be used to determine # the new center and image size at once if center is not None and not isinstance(center, str): xO, yO = getCartesianPoints2(maxRadius, thetas, center) else: xO, yO = getCartesianPoints2(maxRadius, thetas, np.array([0, 0])) # Finally, get the maximum and minimum x/y to obtain the bounds necessary # For the minimum x/y, the largest it can be is 0 because of the origin # For the maximum x/y, the smallest it can be is 0 because of the origin # This happens when the initial and final source angle are in the same quadrant # Because of this, it is guaranteed that the min is <= 0 and max is >= 0 xMin, xMax = min(xO.min(), 0), max(xO.max(), 0) yMin, yMax = min(yO.min(), 0), max(yO.max(), 0) # Set the image size and center based on the x/y min/max if center == 'bottom-left': imageSize = np.array([yMax, xMax]) center = np.array([0, 0]) elif center == 'bottom-middle': imageSize = np.array([yMax, xMax - xMin]) center = np.array([xMin, 0]) elif center == 'bottom-right': imageSize = np.array([yMax, xMin]) center = np.array([xMin, 0]) elif center == 'middle-left': imageSize = np.array([yMax - yMin, xMax]) center = np.array([0, yMin]) elif center == 'middle-middle': imageSize = np.array([yMax - yMin, xMax - xMin]) center = np.array([xMin, yMin]) elif center == 'middle-right': imageSize = np.array([yMax - yMin, xMin]) center = np.array([xMin, yMin]) elif center == 'top-left': imageSize = np.array([yMin, xMax]) center = np.array([0, yMin]) elif center == 'top-middle': imageSize = np.array([yMin, xMax - xMin]) center = np.array([xMin, yMin]) elif center == 'top-right': imageSize = np.array([yMin, xMin]) center = np.array([xMin, yMin]) # When the image size or center are set to x or y min, then that is a negative value # Instead of typing abs for each one, an absolute value of the image size and center is done at the end to # make it easier. imageSize = np.ceil(np.abs(imageSize)).astype(int) center = np.ceil(np.abs(center)).astype(int) elif isinstance(center, str): # Set the center based on the image size given if center == 'bottom-left': center = imageSize[1::-1] * np.array([0, 0]) elif center == 'bottom-middle': center = imageSize[1::-1] * np.array([1 / 2, 0]) elif center == 'bottom-right': center = imageSize[1::-1] * np.array([1, 0]) elif center == 'middle-left': center = imageSize[1::-1] * np.array([0, 1 / 2]) elif center == 'middle-middle': center = imageSize[1::-1] * np.array([1 / 2, 1 / 2]) elif center == 'middle-right': center = imageSize[1::-1] * np.array([1, 1 / 2]) elif center == 'top-left': center = imageSize[1::-1] * np.array([0, 1]) elif center == 'top-middle': center = imageSize[1::-1] * np.array([1 / 2, 1]) elif center == 'top-right': center = imageSize[1::-1] * np.array([1, 1]) # Convert image size to tuple to standardize the variable type # Some people may use list but we want to convert this imageSize = tuple(imageSize) settings = ImageTransform(center, initialRadius, finalRadius, initialAngle, finalAngle, imageSize, image.shape[-2:], hasColor) else: # This is used to scale the result of the radius to get the appropriate Cartesian value scaleRadius = settings.polarImageSize[1] / (settings.finalRadius - settings.initialRadius) # This is used to scale the result of the angle to get the appropriate Cartesian value scaleAngle = settings.polarImageSize[0] / (settings.finalAngle - settings.initialAngle) # Get list of cartesian x and y coordinate and create a 2D create of the coordinates using meshgrid xs = np.arange(0, settings.cartesianImageSize[1]) ys = np.arange(0, settings.cartesianImageSize[0]) x, y = np.meshgrid(xs, ys) # Take cartesian grid and convert to polar coordinates r, theta = getPolarPoints2(x, y, settings.center) # Offset the radius by the initial source radius r = r - settings.initialRadius # Offset the theta angle by the initial source angle # The theta values may go past 2pi, so they are looped back around by taking modulo with 2pi. # Note: This assumes initial source angle is positive theta = np.mod(theta - settings.initialAngle + 2 * np.pi, 2 * np.pi) # Scale the radius using scale factor r = r * scaleRadius # Scale the angle from radians to pixels using scale factor theta = theta * scaleAngle # Flatten the desired x/y cartesian points into one 2xN array desiredCoords = np.vstack((theta.flatten(), r.flatten())) # Get the new shape which is the cartesian image shape plus any other dimensions # Get the new shape of the cartesian image which is the same shape of the polar image except the last two dimensions # (r & theta) are replaced with the cartesian image size newShape = image.shape[:-2] + settings.cartesianImageSize # Reshape the image to be 3D, flattens the array if > 3D otherwise it makes it 3D with the 3rd dimension a size of 1 image = image.reshape((-1,) + settings.polarImageSize) if border == 'constant': # Pad image by 3 pixels and then offset all of the desired coordinates by 3 image = np.pad(image, ((0, 0), (3, 3), (3, 3)), 'edge') desiredCoords += 3 if useMultiThreading: with concurrent.futures.ThreadPoolExecutor() as executor: futures = [executor.submit(scipy.ndimage.map_coordinates, slice, desiredCoords, mode=border, cval=borderVal, order=order) for slice in image] concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED) cartesianImages = [future.result().reshape(x.shape) for future in futures] else: cartesianImages = [] # Loop through the third dimension and map each 2D slice for slice in image: imageSlice = scipy.ndimage.map_coordinates(slice, desiredCoords, mode=border, cval=borderVal, order=order).reshape(x.shape) cartesianImages.append(imageSlice) # Stack all of the slices together and reshape it to what it should be cartesianImage = np.stack(cartesianImages, axis=0).reshape(newShape) # If there is a color channel present, move it abck to the end of axes if settings.hasColor: cartesianImage = np.moveaxis(cartesianImage, 0, -1) return cartesianImage, settings
from polarTransform.pointsConversion import * from polarTransform.imageTransform import ImageTransform