import concurrent.futures
import scipy.ndimage
[docs]def convertToPolarImage(image, center=None, initialRadius=None, finalRadius=None, initialAngle=None, finalAngle=None,
radiusSize=None, angleSize=None, order=3, border='constant', borderVal=0.0,
useMultiThreading=False, settings=None):
"""Convert cartesian image to polar image.
Using a cartesian image, this function creates a polar domain image where the first dimension is radius and
second dimension is the angle. This function is versatile because it allows different starting and stopping
radii and angles to extract the polar region you are interested in.
.. note::
Traditionally images are loaded such that the origin is in the upper-left hand corner. In these cases the
:obj:`initialAngle` and :obj:`finalAngle` will rotate clockwise from the x-axis. For simplicitly, it is
recommended to flip the image along first dimension before passing to this function.
Parameters
----------
image : (N, M) or (N, M, P) :class:`numpy.ndarray`
Cartesian image to convert to polar domain
.. note::
For a 3D array, polar transformation is applied separately across each 2D slice
.. 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.
center : (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.
The center is structured as (x, y) where the first item is the x-coordinate and second item is the y-coordinate.
If center is not set, then it will default to ``round(image.shape[::-1] / 2)``.
initialRadius : :class:`int`, optional
Starting radius in pixels from the center of the cartesian image that will appear in the polar image
The polar image will begin at this radius, i.e. the first row of the polar image will correspond 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 that will appear in the polar image
The polar image will end at this radius, i.e. the last row of the polar image will correspond to this ending
radius.
.. note::
The polar image will **not** include this radius. It will include 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 of the cartesian image. Using the
furthest corner from the center, the finalRadius can be calculated as:
.. math::
finalRadius = \\sqrt{((X_{max} - X_{center})^2 + (Y_{max} - Y_{center})^2)}
initialAngle : :class:`float`, optional
Starting angle in radians that will appear in the polar image
The polar image will begin at this angle, i.e. the first column of the polar image will correspond 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 that will appear in the polar image
The polar image will end at this angle, i.e. the last column of the polar image will correspond to this
ending angle.
.. note::
The polar image will **not** include this angle. It will include all angle starting
from initial to final angle **excluding** the final angle. Rather, it will stop 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`.
radiusSize : :class:`int`, optional
Size of polar image for radial (1st) dimension
This in effect determines the resolution of the radial dimension of the polar image based on the
:obj:`initialRadius` and :obj:`finalRadius`. Resolution can be calculated using equation below in radial
px per cartesian px:
.. math::
radialResolution = \\frac{radiusSize}{finalRadius - initialRadius}
If radiusSize is not set, then it will default to the minimum size necessary to ensure that image information
is not lost in the transformation. The minimum resolution necessary can be found by finding the smallest
change in radius from two connected pixels in the cartesian image. Through experimentation, there is a
surprisingly close relationship between the maximum difference from width or height of the cartesian image to
the :obj:`center` times two.
The radiusSize is calculated based on this relationship and is proportional to the :obj:`initialRadius` and
:obj:`finalRadius` given.
angleSize : :class:`int`, optional
Size of polar image for angular (2nd) dimension
This in effect determines the resolution of the angular dimension of the polar image based on the
:obj:`initialAngle` and :obj:`finalAngle`. Resolution can be calculated using equation below in angular
px per cartesian px:
.. math::
angularResolution = \\frac{angleSize}{finalAngle - initialAngle}
If angleSize is not set, then it will default to the minimum size necessary to ensure that image information
is not lost in the transformation. The minimum resolution necessary can be found by finding the smallest
change in angle from two connected pixels in the cartesian image.
For a cartesian image with either dimension greater than 500px, the angleSize is set to be **two** times larger
than the largest dimension proportional to :obj:`initialAngle` and :obj:`finalAngle`. Otherwise, for a
cartesian image with both dimensions less than 500px, the angleSize is set to be **four** times larger the
largest dimension proportional to :obj:`initialAngle` and :obj:`finalAngle`.
.. note::
The above logic **estimates** the necessary angleSize to reduce image information loss. No algorithm
currently exists for determining the required angleSize.
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.convertToPolarImage`
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
-------
polarImage : (N, M) or (N, M, P) :class:`numpy.ndarray`
Polar image where first dimension is radii and second dimension is angle (3D polar image if 3D input image
is given)
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.
"""
# Create settings if none are given
if settings is None:
# If center is not specified, set to the center of the image
# Image shape is reversed because center is specified as x,y and shape is r,c.
# Otherwise, make sure the center is a Numpy array
if center is None:
center = (np.array(image.shape[1::-1]) / 2).astype(int)
else:
center = np.array(center)
# Initial radius is zero if none is selected
if initialRadius is None:
initialRadius = 0
# Calculate the maximum radius possible
# Get four corners (indices) of the cartesian image
# Convert the corners to polar and get the largest radius
# This will be the maximum radius to represent the entire image in polar
corners = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) * image.shape[0:2]
radii, _ = getPolarPoints2(corners[:, 1], corners[:, 0], center)
maxRadius = np.ceil(radii.max()).astype(int)
if finalRadius is None:
finalRadius = maxRadius
# Initial angle of zero if none is selected
if initialAngle is None:
initialAngle = 0
# Final radius is the size of the image so that all points from cartesian are on the polar image
# Final angle is 2pi to loop throughout entire image
if finalAngle is None:
finalAngle = 2 * np.pi
# If no radius size is given, then the size will be set to make the radius size twice the size of the largest
# dimension of the image
# There is a surprisingly close relationship between the maximum difference from
# width/height of image to center times two.
# The radius size is proportional to the final radius and initial radius
if radiusSize is None:
cross = np.array([[image.shape[1] - 1, center[1]], [0, center[1]], [center[0], image.shape[0] - 1],
[center[0], 0]])
radiusSize = np.ceil(np.abs(cross - center).max() * 2 * (finalRadius - initialRadius) / maxRadius) \
.astype(int)
# Make the angle size be twice the size of largest dimension for images above 500px, otherwise
# use a factor of 4x.
# This angle size is proportional to the initial and final angle.
# This was experimentally determined to yield the best resolution
# The actual answer for the necessary angle size to represent all of the pixels is
# (finalAngle - initialAngle) / (min(arctan(y / x) - arctan((y - 1) / x)))
# Where the coordinates used in min are the four corners of the cartesian image with the center
# subtracted from it. The minimum will be the corner that is the furthest away from the center
# TODO Find a better solution to determining default angle size (optimum?)
if angleSize is None:
maxSize = np.max(image.shape)
if maxSize > 500:
angleSize = int(2 * np.max(image.shape) * (finalAngle - initialAngle) / (2 * np.pi))
else:
angleSize = int(4 * np.max(image.shape) * (finalAngle - initialAngle) / (2 * np.pi))
# Create the settings
settings = ImageTransform(center, initialRadius, finalRadius, initialAngle, finalAngle, image.shape[0:2],
(radiusSize, angleSize))
# Create radii from start to finish with radiusSize, do same for theta
# Then create a 2D grid of radius and theta using meshgrid
# Set endpoint to False to NOT include the final sample specified. Think of it like this, if you ask to count from
# 0 to 30, that is 31 numbers not 30. Thus, we count 0...29 to get 30 numbers.
radii = np.linspace(settings.initialRadius, settings.finalRadius, settings.polarImageSize[0], endpoint=False)
theta = np.linspace(settings.initialAngle, settings.finalAngle, settings.polarImageSize[1], endpoint=False)
r, theta = np.meshgrid(radii, theta)
# Take polar grid and convert to cartesian coordinates
xCartesian, yCartesian = getCartesianPoints2(r, theta, settings.center)
# Flatten the desired x/y cartesian points into one 2xN array
desiredCoords = np.vstack((yCartesian.flatten(), xCartesian.flatten()))
# Get the new shape which is the cartesian image shape plus any other dimensions
newShape = settings.polarImageSize + image.shape[2:]
# 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(image.shape[0:2] + (-1,))
# If border is set to constant, then pad the image by the edges by 3 pixels.
# If one tries to convert back to cartesian without the borders padded then the border of the cartesian image will
# be corrupted because it will average the pixels with the border value
if border == 'constant':
# Pad image by 3 pixels and then offset all of the desired coordinates by 3
image = np.pad(image, ((3, 3), (3, 3), (0, 0)), 'edge')
desiredCoords += 3
if useMultiThreading:
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [executor.submit(scipy.ndimage.map_coordinates, image[:, :, k], desiredCoords, mode=border,
cval=borderVal, order=order) for k in range(image.shape[2])]
concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED)
polarImages = [future.result().reshape(r.shape).T for future in futures]
else:
polarImages = []
# Loop through the third dimension and map each 2D slice
for k in range(image.shape[2]):
imageSlice = scipy.ndimage.map_coordinates(image[:, :, k], desiredCoords, mode=border, cval=borderVal,
order=order).reshape(r.shape).T
polarImages.append(imageSlice)
# Stack all of the slices together and reshape it to what it should be
polarImage = np.dstack(polarImages).reshape(newShape)
return polarImage, settings
from polarTransform.imageTransform import ImageTransform
from polarTransform.pointsConversion import *