diff --git a/.gitignore b/.gitignore index 70d5064..cce4e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,7 @@ ENV/ *.iml # Ignore dist folder which contains the Python compiled code for creating a standalone executable -dist \ No newline at end of file +dist + +# Ignore vscode settings +.vscode \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 940af30..7f9218a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,18 @@ language: python + python: - 3.6 -install: pip install -r requirements.txt -script: python tests/test_polarTransform.py + +# Command to install dependencies +install: + - pip install --upgrade pip + - pip install -r requirements.txt + - pip install -r requirements-dev.txt + +# Command to run tests +script: coverage run -m unittest discover -v polarTransform/tests + +# Command to deploy releases to PyPi deploy: provider: pypi distributions: sdist bdist_wheel @@ -11,3 +21,15 @@ deploy: tags: true password: secure: F0Xx96xWhRdtfD3kSywX4VqF2VbZst85Wy0AsI8774OeN3e2712lgQyRp1sBp4AZzsYWowKg4/kFNljvJtko56Ag+ulwLieP9K+idWj4kc62BKCBMfsTEhHWicAla3mnlfIQGMIvD9En7CUPAiu5Bs+wj8kto2LRwv2lztSKBN8fk56r9zTd7f6Ft8MOoZqXZ8cpQNv610BvsY61s/u2RnaDkcI75LyztPCGIQ3O1wYnZWNebslif9QMsygHvsSFxoomOhM/MjRvsvEwpzDapKdaQ7xYf9uBfw40/XG/9xL0JqAjXpbKNltxFaPkshVgDBt+xMStyYV2nd5LClPrYYpSBHeFtL+HsO/lnUksBs09GwLQFw0TYfed0E28seMeb6zTSlnFQF2VnxVWu1Vqe1uriMmSH/0iKaes6ucQZ6Oag22ub3mtABz855kQ5FtkVW2eK8MG8FoFoxi6x+9e+YN0BG9A2NsNlNvur+nzx5XTduGVs+REAHURZ/56WG8IsKqOpRrNHPL5cK3+FBwCnAZVwkR5cT9K4pKtJBiL4h0aJzPTi3F4MCSns2vMbLSMQMBHVBey+lmReOZ9GH7uzS3KOZa+5LJLEFkvQjb3N3X7znn0oJO7s/aSsW2bzjrRhsYPYoLvSkcPqmyswowesB/SrKzX3vzvgBT9DFsCAHI= + +after_success: + - codecov + +# Only build for master branch and tagged released (must follow semantic versioning syntax) +branches: + only: + - master + - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ + + + diff --git a/MANIFEST.in b/MANIFEST.in index b1eaf85..d89bcdf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.rst -include LICENSE \ No newline at end of file +include LICENSE +include polarTransform/tests/data/* \ No newline at end of file diff --git a/README.rst b/README.rst index d5070f9..92058e5 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,24 @@ +.. image:: https://travis-ci.org/addisonElliott/polarTransform.svg?branch=master + :target: https://travis-ci.org/addisonElliott/polarTransform + :alt: Build Status + +.. image:: https://img.shields.io/pypi/pyversions/polarTransform.svg + :target: https://img.shields.io/pypi/pyversions/polarTransform.svg + :alt: Python version + +.. image:: https://badge.fury.io/py/polarTransform.svg + :target: https://badge.fury.io/py/polarTransform + :alt: PyPi version + +.. image:: https://readthedocs.org/projects/polartransform/badge/?version=latest + :target: https://polartransform.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. image:: https://codecov.io/gh/addisonElliott/polarTransform/branch/master/graph/badge.svg + :target: https://codecov.io/gh/addisonElliott/polarTransform + +| + Introduction ================= polarTransform is a Python package for converting images between the polar and Cartesian domain. It contains many @@ -43,9 +64,11 @@ the updated polarTransform code. Test and coverage ================= -To test the code on any platform, make sure to clone the GitHub repository to get the tests and:: +Run the following command in the base directory to run the tests: + +.. code-block:: bash - python tests/test_polarTransform.py + python -m unittest discover -v polarTransform/tests Example ================= @@ -74,12 +97,12 @@ Input image: plt.figure() plt.imshow(cartesianImage, origin='lower') -Resulting polar domain image: +The result is a polar domain image with a specified initial and final radius and angle: .. image:: http://polartransform.readthedocs.io/en/latest/_images/verticalLinesPolarImage_scaled3.png :alt: Polar image -Converting back to the cartesian image results in: +Converting back to the cartesian image results in only a slice of the original image to be shown because the initial and final radius and angle were specified: .. image:: http://polartransform.readthedocs.io/en/latest/_images/verticalLinesCartesianImage_scaled.png :alt: Cartesian image diff --git a/docs/source/conf.py b/docs/source/conf.py index 220925a..fd5ea16 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,6 +20,7 @@ import os import sys sys.path.insert(0, os.path.abspath('../../')) +import polarTransform def setup(app): app.add_stylesheet('css/custom.css') @@ -56,6 +57,9 @@ def setup(app): # The master toctree document. master_doc = 'index' +# Fixes issue with toctree reference to nonexisting document... +numpydoc_show_class_members = False + # General information about the project. project = 'polarTransform' copyright = '2018, Addison Elliott' @@ -66,9 +70,9 @@ def setup(app): # built documents. # # The short X.Y version. -version = '1.0' +version = polarTransform.__version__ # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = polarTransform.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -80,7 +84,7 @@ def setup(app): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = [] +exclude_patterns = ['polarTransform.tests.rst'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index 9af58f6..967078a 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -2,8 +2,6 @@ Getting Started ================ -.. rubric:: Brief overview of polarTransform and how to install. - Introduction ============ polarTransform is a Python package for converting images between the polar and Cartesian domain. It contains many @@ -53,9 +51,11 @@ the updated polarTransform code. Test and coverage ================= -To test the code on any platform, make sure to clone the GitHub repository to get the tests and:: +Run the following command in the base directory to run the tests: + +.. code-block:: bash - python tests/test_polarTransform.py + python -m unittest discover -v polarTransform/tests Using polarTransform ==================== diff --git a/docs/source/polarTransform.rst b/docs/source/polarTransform.rst index a61e781..0123c29 100644 --- a/docs/source/polarTransform.rst +++ b/docs/source/polarTransform.rst @@ -1,7 +1,20 @@ -=============== Reference Guide =============== +Table of Contents +----------------- + +.. autosummary:: + + polarTransform.convertToCartesianImage + polarTransform.convertToPolarImage + polarTransform.getCartesianPointsImage + polarTransform.getPolarPointsImage + polarTransform.ImageTransform + +polarTransform Module +--------------------- + .. automodule:: polarTransform :members: :undoc-members: diff --git a/docs/source/user-guide.rst b/docs/source/user-guide.rst index 1029f14..0daaca3 100644 --- a/docs/source/user-guide.rst +++ b/docs/source/user-guide.rst @@ -4,7 +4,11 @@ User Guide .. currentmodule:: polarTransform -:class:`convertToPolarImage` and :class:`convertToCartesianImage` are the two primary classes on which this entire project is based on. The two functions are opposites of one another, reversing the action that the other function does. +:class:`convertToPolarImage` and :class:`convertToCartesianImage` are the two primary functions that make up this package. The two functions are opposites of one another, reversing the action that the other function does. + +As the names suggest, the two functions convert an image from the cartesian or polar domain to the other domain with a given set of parameters. The power of these functions is that the user can specify the resulting image resolution, interpolation order, initial and final radii or angles and much much more. See the :doc:`polarTransform` for more information on the specific parameters that are supported. + +Since there are quite a few parameters that can be specified for the conversion functions, the class :class:`ImageTransform` is created and returned from the :class:`convertToPolarImage` or :class:`convertToCartesianImage` functions (along with the converted image) that contains the arguments specified. The benefit of this class is that if one wants to convert the image back to another domain or convert points on either image to/from the other domain, they can simply call the functions within the :class:`ImageTransform` class without specifying all of the arguments again. Example 1 -------------- diff --git a/polarTransform.py b/polarTransform.py deleted file mode 100644 index 341bc78..0000000 --- a/polarTransform.py +++ /dev/null @@ -1,1199 +0,0 @@ -import numpy as np -import scipy.interpolate -import scipy.ndimage -import skimage.util - - -class ImageTransform: - def __init__(self, center, initialRadius, finalRadius, initialAngle, finalAngle, cartesianImageSize, - polarImageSize): - """Polar and Cartesian Transform Metadata - - ImageTransform contains polar and cartesian transform metadata for the conversion between the two domains. - This metadata is stored in a class to allow for easy conversion between the domains. - - Parameters - ---------- - center : (2,) :class:`numpy.ndarray` of :class:`int` - 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. - initialRadius : :class:`int` - 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. - 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. - 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`. - 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. - - 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`. - cartesianImageSize : (2,) :class:`tuple` of :class:`int` - Size of cartesian image - polarImageSize : (2,) :class:`tuple` of :class:`int` - Size of polar image - """ - self.center = center - self.initialRadius = initialRadius - self.finalRadius = finalRadius - self.initialAngle = initialAngle - self.finalAngle = finalAngle - self.cartesianImageSize = cartesianImageSize - self.polarImageSize = polarImageSize - - def convertToPolarImage(self, image, order=3, border='constant', borderVal=0.0): - """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, 3) or (N, M, 4) :class:`numpy.ndarray` - Cartesian image to convert to polar domain - - .. note:: - If an alpha band (4th channel of image is present, then it will be ignored during polar conversion. The - resulting polar image will contain four channels but the alpha channel will be all 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 - - Returns - ------- - polarImage : (N, M, 3) or (N, M, 4) :class:`numpy.ndarray` - Polar image where first dimension is radii and second dimension is angle - """ - image, ptSettings = convertToPolarImage(image, order=order, border=border, borderVal=borderVal, settings=self) - return image - - def convertToCartesianImage(self, image, order=3, border='constant', borderVal=0.0): - """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 appropiate 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, M, 3) or (N, M, 4) :class:`numpy.ndarray` - Polar image to convert to cartesian domain - - .. note:: - If an alpha band (4th channel of image is present, then it will be ignored during cartesian conversion. - The resulting polar image will contain four channels but the alpha channel will be all 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 - - Returns - ------- - cartesianImage : (N, M, 3) or (N, M, 4) :class:`numpy.ndarray` - Cartesian image - - See Also - -------- - :meth:`convertToCartesianImage` - """ - image, ptSettings = convertToCartesianImage(image, order=order, border=border, borderVal=borderVal, - settings=self) - return image - - def getPolarPointsImage(self, points): - """Convert list of cartesian points from image to polar image points based on transform metadata - - .. note:: - This does **not** convert from cartesian to polar points, but rather converts pixels from cartesian image to - pixels from polar image using :class:`ImageTransform`. - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - points : (N, 2) or (2,) :class:`numpy.ndarray` - List of cartesian points to convert to polar domain - - First column is x and second column is y - - Returns - ------- - polarPoints : (N, 2) or (2,) :class:`numpy.ndarray` - Corresponding polar points from cartesian :obj:`points` using :class:`ImageTransform` - - See Also - -------- - :meth:`getPolarPointsImage`, :meth:`getPolarPoints`, :meth:`getPolarPoints2` - """ - - return getPolarPointsImage(points, self) - - def getCartesianPointsImage(self, points): - """Convert list of polar points from image to cartesian image points based on transform metadata - - .. note:: - This does **not** convert from polar to cartesian points, but rather converts pixels from polar image to - pixels from cartesian image using :class:`ImageTransform`. - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - points : (N, 2) or (2,) :class:`numpy.ndarray` - List of polar points to convert to cartesian domain - - First column is r and second column is theta - - Returns - ------- - cartesianPoints : (N, 2) or (2,) :class:`numpy.ndarray` - Corresponding cartesian points from polar :obj:`points` using :class:`ImageTransform` - - See Also - -------- - :meth:`getCartesianPointsImage`, :meth:`getCartesianPoints`, :meth:`getCartesianPoints2` - """ - return getCartesianPointsImage(points, self) - - def __repr__(self): - return 'ImageTransform(center=%s, initialRadius=%i, finalRadius=%i, initialAngle=%f, finalAngle=%f, ' \ - 'cartesianImageSize=%s, polarImageSize=%s)' % ( - self.center, self.initialRadius, self.finalRadius, self.initialAngle, self.finalAngle, - self.cartesianImageSize, self.polarImageSize) - - def __str__(self): - return self.__repr__() - - -def getCartesianPoints(rTheta, center): - """Convert list of polar points to cartesian points - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - rTheta : (N, 2) or (2,) :class:`numpy.ndarray` - List of cartesian points to convert to polar domain - - First column is r and second column is theta - center : (2,) :class:`numpy.ndarray` - Center to use for conversion to cartesian domain of polar points - - Format of center is (x, y) - - Returns - ------- - cartesianPoints : (N, 2) :class:`numpy.ndarray` - Corresponding cartesian points from cartesian :obj:`rTheta` - - First column is x and second column is y - - See Also - -------- - :meth:`getCartesianPoints2` - """ - if rTheta.ndim == 2: - x = rTheta[:, 0] * np.cos(rTheta[:, 1]) + center[0] - y = rTheta[:, 0] * np.sin(rTheta[:, 1]) + center[1] - else: - x = rTheta[0] * np.cos(rTheta[1]) + center[0] - y = rTheta[0] * np.sin(rTheta[1]) + center[1] - - return np.array([x, y]).T - - -def getCartesianPoints2(r, theta, center): - """Convert list of polar points to cartesian points - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - r : (N,) :class:`numpy.ndarray` - List of polar r points to convert to cartesian domain - theta : (N,) :class:`numpy.ndarray` - List of polar theta points to convert to cartesian domain - center : (2,) :class:`numpy.ndarray` - Center to use for conversion to cartesian domain of polar points - - Format of center is (x, y) - - Returns - ------- - x : (N,) :class:`numpy.ndarray` - Corresponding x points from polar :obj:`r` and :obj:`theta` - y : (N,) :class:`numpy.ndarray` - Corresponding y points from polar :obj:`r` and :obj:`theta` - - See Also - -------- - :meth:`getCartesianPoints` - """ - x = r * np.cos(theta) + center[0] - y = r * np.sin(theta) + center[1] - - return x, y - - -def getPolarPoints(xy, center): - """Convert list of cartesian points to polar points - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - xy : (N, 2) or (2,) :class:`numpy.ndarray` - List of cartesian points to convert to polar domain - - First column is x and second column is y - center : (2,) :class:`numpy.ndarray` - Center to use for conversion to polar domain of cartesian points - - Format of center is (x, y) - - Returns - ------- - polarPoints : (N, 2) :class:`numpy.ndarray` - Corresponding polar points from cartesian :obj:`xy` - - First column is r and second column is theta - - See Also - -------- - :meth:`getPolarPoints2` - """ - if xy.ndim == 2: - cX, cY = xy[:, 0] - center[0], xy[:, 1] - center[1] - else: - cX, cY = xy[0] - center[0], xy[1] - center[1] - - r = np.sqrt(cX ** 2 + cY ** 2) - theta = np.arctan2(cY, cX) - - # Make range of theta 0 -> 2pi instead of -pi -> pi - # According to StackOverflow, this is the fastest method: - # https://stackoverflow.com/questions/37358016/numpy-converting-range-of-angles-from-pi-pi-to-0-2pi - theta = np.where(theta < 0, theta + 2 * np.pi, theta) - - return np.array([r, theta]).T - - -def getPolarPoints2(x, y, center): - """Convert list of cartesian points to polar points - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - x : (N,) :class:`numpy.ndarray` - List of cartesian x points to convert to polar domain - y : (N,) :class:`numpy.ndarray` - List of cartesian y points to convert to polar domain - center : (2,) :class:`numpy.ndarray` - Center to use for conversion to polar domain of cartesian points - - Format of center is (x, y) - - Returns - ------- - r : (N,) :class:`numpy.ndarray` - Corresponding radii points from cartesian :obj:`x` and :obj:`y` - theta : (N,) :class:`numpy.ndarray` - Corresponding theta points from cartesian :obj:`x` and :obj:`y` - - See Also - -------- - :meth:`getPolarPoints` - """ - cX, cY = x - center[0], y - center[1] - - r = np.sqrt(cX ** 2 + cY ** 2) - - theta = np.arctan2(cY, cX) - - # Make range of theta 0 -> 2pi instead of -pi -> pi - # According to StackOverflow, this is the fastest method: - # https://stackoverflow.com/questions/37358016/numpy-converting-range-of-angles-from-pi-pi-to-0-2pi - theta = np.where(theta < 0, theta + 2 * np.pi, theta) - - return r, theta - - -def getPolarPointsImage(points, settings): - """Convert list of cartesian points from image to polar image points based on transform metadata - - .. warning:: - Cleaner and more succinct to use :meth:`ImageTransform.getPolarPointsImage` - - .. note:: - This does **not** convert from cartesian to polar points, but rather converts pixels from cartesian image to - pixels from polar image using :class:`ImageTransform`. - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - points : (N, 2) or (2,) :class:`numpy.ndarray` - List of cartesian points to convert to polar domain - - First column is x and second column is y - settings : :class:`ImageTransform` - Contains metadata for conversion from polar to cartesian domain - - 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. - - Returns - ------- - polarPoints : (N, 2) or (2,) :class:`numpy.ndarray` - Corresponding polar points from cartesian :obj:`points` using :obj:`settings` - - See Also - -------- - :meth:`ImageTransform.getPolarPointsImage`, :meth:`getPolarPoints`, :meth:`getPolarPoints2` - """ - # Convert points to NumPy array - points = np.asanyarray(points) - - # If there is only one point specified and number of dimensions is only one, then make the array a 1x2 array so that - # points[:, 0/1] will not throw an error - if points.ndim == 1 and points.shape[0] == 2: - points = np.expand_dims(points, axis=0) - needSqueeze = True - else: - needSqueeze = False - - # This is used to scale the result of the radius to get the appropriate Cartesian value - scaleRadius = settings.polarImageSize[0] / (settings.finalRadius - settings.initialRadius) - - # This is used to scale the result of the angle to get the appropriate Cartesian value - scaleAngle = settings.polarImageSize[1] / (settings.finalAngle - settings.initialAngle) - - # Take cartesian grid and convert to polar coordinates - polarPoints = getPolarPoints(points, settings.center) - - # Offset the radius by the initial source radius - polarPoints[:, 0] = polarPoints[:, 0] - 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 - initialAngle + 2 * np.pi, 2 * np.pi) - polarPoints[:, 1] = np.mod(polarPoints[:, 1] - settings.initialAngle + 2 * np.pi, 2 * np.pi) - - # Scale the radius using scale factor - # Scale the angle from radians to pixels using scale factor - polarPoints = polarPoints * [scaleRadius, scaleAngle] - - if needSqueeze: - return np.squeeze(polarPoints) - else: - return polarPoints - - -def getCartesianPointsImage(points, settings): - """Convert list of polar points from image to cartesian image points based on transform metadata - - .. warning:: - Cleaner and more succinct to use :meth:`ImageTransform.getCartesianPointsImage` - - .. note:: - This does **not** convert from polar to cartesian points, but rather converts pixels from polar image to - pixels from cartesian image using :class:`ImageTransform`. - - The returned points are not rounded to the nearest point. User must do that by hand if desired. - - Parameters - ---------- - points : (N, 2) or (2,) :class:`numpy.ndarray` - List of polar points to convert to cartesian domain - - First column is r and second column is theta - settings : :class:`ImageTransform` - Contains metadata for conversion from polar to cartesian domain - - 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. - - Returns - ------- - cartesianPoints : (N, 2) or (2,) :class:`numpy.ndarray` - Corresponding cartesian points from polar :obj:`points` using :obj:`settings` - - See Also - -------- - :meth:`ImageTransform.getCartesianPointsImage`, :meth:`getCartesianPoints`, :meth:`getCartesianPoints2` - """ - # Convert points to NumPy array - points = np.asanyarray(points) - - # If there is only one point specified and number of dimensions is only one, then make the array a 1x2 array so that - # points[:, 0/1] will not throw an error - if points.ndim == 1 and points.shape[0] == 2: - points = np.expand_dims(points, axis=0) - needSqueeze = True - else: - needSqueeze = False - - # This is used to scale the result of the radius to get the appropriate Cartesian value - scaleRadius = settings.polarImageSize[0] / (settings.finalRadius - settings.initialRadius) - - # This is used to scale the result of the angle to get the appropriate Cartesian value - scaleAngle = settings.polarImageSize[1] / (settings.finalAngle - settings.initialAngle) - - # Create a new copy of the points variable because we are going to change it and don't want the points parameter to - # change outside of this function - points = points.copy() - - # Scale the radius using scale factor - # Scale the angle from radians to pixels using scale factor - points = points / [scaleRadius, scaleAngle] - - # Offset the radius by the initial source radius - points[:, 0] = points[:, 0] + 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 - initialAngle + 2 * np.pi, 2 * np.pi) - points[:, 1] = np.mod(points[:, 1] + settings.initialAngle + 2 * np.pi, 2 * np.pi) - - # Take cartesian grid and convert to polar coordinates - cartesianPoints = getCartesianPoints(points, settings.center) - - if needSqueeze: - return np.squeeze(cartesianPoints) - else: - return cartesianPoints - - -def convertToPolarImage(image, center=None, initialRadius=None, finalRadius=None, initialAngle=None, finalAngle=None, - radiusSize=None, angleSize=None, order=3, border='constant', borderVal=0.0, - 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, 3) or (N, M, 4) :class:`numpy.ndarray` - Cartesian image to convert to polar domain - - .. note:: - If an alpha band (4th channel of image is present, then it will be ignored during polar conversion. The - resulting polar image will contain four channels but the alpha channel will be all 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 - 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, 3) or (N, M, 4) :class:`numpy.ndarray` - Polar image where first dimension is radii and second dimension is angle - 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. - """ - - # Determines whether there are multiple bands or channels in image by checking for 3rd dimension - isMultiChannel = image.ndim == 3 - - # 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())) - - # 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)) if isMultiChannel else 3, 'edge') - desiredCoords += 3 - - # Retrieve polar image using map_coordinates. Returns a linear array of the values that - # must be reshaped into the desired size - # For multiple channels, repeat this process for each band and concatenate them at end - # Take the transpose of the polar image such that first dimension is radius and second - # dimension is theta. - if isMultiChannel: - polarImages = [] - - # Assume that there are at least 3 bands in 3D matrix - for k in range(3): - polarImage = scipy.ndimage.map_coordinates(image[:, :, k], desiredCoords, mode=border, cval=borderVal, - order=order).reshape(r.shape).T - polarImages.append(polarImage) - - # If there are 4 bands, then assume the 4th band is alpha - # We do not want to interpolate the transparency so we just make it all fully opaque - if image.shape[2] == 4: - imin, imax = skimage.util.dtype_limits(polarImages[0], False) - polarImage = np.full_like(polarImages[0], imax) - polarImages.append(polarImage) - - polarImage = np.dstack(polarImages) - else: - polarImage = scipy.ndimage.map_coordinates(image, desiredCoords, mode=border, cval=borderVal, - order=order).reshape(r.shape).T - - return polarImage, settings - - -def convertToCartesianImage(image, center=None, initialRadius=None, - finalRadius=None, initialAngle=None, - finalAngle=None, imageSize=None, order=3, border='constant', - borderVal=0.0, 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 appropiate 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, M, 3) or (N, M, 4) :class:`numpy.ndarray` - Polar image to convert to cartesian domain - - .. note:: - If an alpha band (4th channel of image is present, then it will be ignored during cartesian conversion. The - resulting polar image will contain four channels but the alpha channel will be all fully on. - 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. - 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 - 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, M, 3) or (N, M, 4) :class:`numpy.ndarray` - Cartesian image - 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. - """ - # Determines whether there are multiple bands or channels in image by checking for 3rd dimension - isMultiChannel = image.ndim == 3 - - 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[0] - - # 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[0] / (finalRadius - initialRadius) - - # This is used to scale the result of the angle to get the appropriate Cartesian value - scaleAngle = image.shape[1] / (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[1]) + 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[0:2]) - else: - # This is used to scale the result of the radius to get the appropriate Cartesian value - scaleRadius = settings.polarImageSize[0] / (settings.finalRadius - settings.initialRadius) - - # This is used to scale the result of the angle to get the appropriate Cartesian value - scaleAngle = settings.polarImageSize[1] / (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((r.flatten(), theta.flatten())) - - # 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)) if isMultiChannel else 3, 'edge') - desiredCoords += 3 - - # Retrieve cartesian image using map_coordinates. Returns a linear array of the values that - # must be reshaped into the desired size. - # For multiple channels, repeat this process for each band and concatenate them at end - if isMultiChannel: - cartesianImages = [] - - # Assume that there are at least 3 bands in 3D matrix - for k in range(3): - cartesianImage = scipy.ndimage.map_coordinates(image[:, :, k], desiredCoords, mode=border, cval=borderVal, - order=order).reshape(x.shape) - cartesianImages.append(cartesianImage) - - # If there are 4 bands, then assume the 4th band is alpha - # We do not want to interpolate the transparency so we just make it all fully opaque - if image.shape[2] == 4: - imin, imax = skimage.util.dtype_limits(cartesianImages[0], False) - cartesianImage = np.full_like(cartesianImages[0], imax) - cartesianImages.append(cartesianImage) - - cartesianImage = np.dstack(cartesianImages) - else: - cartesianImage = scipy.ndimage.map_coordinates(image, desiredCoords, mode=border, cval=borderVal, - order=order).reshape(x.shape) - - return cartesianImage, settings diff --git a/polarTransform/__init__.py b/polarTransform/__init__.py new file mode 100644 index 0000000..ffb9c55 --- /dev/null +++ b/polarTransform/__init__.py @@ -0,0 +1,8 @@ +from polarTransform._version import __version__ +from polarTransform.convertToCartesianImage import convertToCartesianImage +from polarTransform.convertToPolarImage import convertToPolarImage +from polarTransform.imageTransform import ImageTransform +from polarTransform.pointsConversion import getCartesianPointsImage, getPolarPointsImage + +__all__ = ['convertToCartesianImage', 'convertToPolarImage', 'ImageTransform', 'getCartesianPointsImage', + 'getPolarPointsImage' '__version__'] diff --git a/polarTransform/_version.py b/polarTransform/_version.py new file mode 100644 index 0000000..a6221b3 --- /dev/null +++ b/polarTransform/_version.py @@ -0,0 +1 @@ +__version__ = '1.0.2' diff --git a/polarTransform/convertToCartesianImage.py b/polarTransform/convertToCartesianImage.py new file mode 100644 index 0000000..f6db183 --- /dev/null +++ b/polarTransform/convertToCartesianImage.py @@ -0,0 +1,360 @@ +import concurrent.futures + +import scipy.ndimage + + +def convertToCartesianImage(image, center=None, initialRadius=None, + finalRadius=None, initialAngle=None, + finalAngle=None, imageSize=None, 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, M) or (N, M, P) :class:`numpy.ndarray` + Polar image to convert to cartesian 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 : :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. + 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, M) or (N, M, P) :class:`numpy.ndarray` + Cartesian image (3D cartesian 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: + # 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[0] + + # 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[0] / (finalRadius - initialRadius) + + # This is used to scale the result of the angle to get the appropriate Cartesian value + scaleAngle = image.shape[1] / (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[1]) + 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[0:2]) + else: + # This is used to scale the result of the radius to get the appropriate Cartesian value + scaleRadius = settings.polarImageSize[0] / (settings.finalRadius - settings.initialRadius) + + # This is used to scale the result of the angle to get the appropriate Cartesian value + scaleAngle = settings.polarImageSize[1] / (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((r.flatten(), theta.flatten())) + + # Get the new shape which is the cartesian image shape plus any other dimensions + newShape = settings.cartesianImageSize + 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 == '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) + + cartesianImages = [future.result().reshape(x.shape) for future in futures] + else: + cartesianImages = [] + + # 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(x.shape) + cartesianImages.append(imageSlice) + + # Stack all of the slices together and reshape it to what it should be + cartesianImage = np.dstack(cartesianImages).reshape(newShape) + + return cartesianImage, settings + + +from polarTransform.pointsConversion import * +from polarTransform.imageTransform import ImageTransform diff --git a/polarTransform/convertToPolarImage.py b/polarTransform/convertToPolarImage.py new file mode 100644 index 0000000..771c684 --- /dev/null +++ b/polarTransform/convertToPolarImage.py @@ -0,0 +1,312 @@ +import concurrent.futures + +import scipy.ndimage + + +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 * diff --git a/polarTransform/imageTransform.py b/polarTransform/imageTransform.py new file mode 100644 index 0000000..e34ee8e --- /dev/null +++ b/polarTransform/imageTransform.py @@ -0,0 +1,269 @@ +class ImageTransform: + """Class to store settings when converting between cartesian and polar domain""" + + def __init__(self, center, initialRadius, finalRadius, initialAngle, finalAngle, cartesianImageSize, + polarImageSize): + """Polar and Cartesian Transform Metadata + + ImageTransform contains polar and cartesian transform metadata for the conversion between the two domains. + This metadata is stored in a class to allow for easy conversion between the domains. + + Parameters + ---------- + center : (2,) :class:`numpy.ndarray` of :class:`int` + 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. + initialRadius : :class:`int` + 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. + 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. + 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`. + 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. + + 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`. + cartesianImageSize : (2,) :class:`tuple` of :class:`int` + Size of cartesian image + polarImageSize : (2,) :class:`tuple` of :class:`int` + Size of polar image + """ + self.center = center + self.initialRadius = initialRadius + self.finalRadius = finalRadius + self.initialAngle = initialAngle + self.finalAngle = finalAngle + self.cartesianImageSize = cartesianImageSize + self.polarImageSize = polarImageSize + + def convertToPolarImage(self, image, order=3, border='constant', borderVal=0.0, useMultiThreading=False): + """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. + 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 + + 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) + """ + image, ptSettings = convertToPolarImage(image, order=order, border=border, borderVal=borderVal, + useMultiThreading=useMultiThreading, settings=self) + return image + + def convertToCartesianImage(self, image, order=3, border='constant', borderVal=0.0, useMultiThreading=False): + """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 appropiate 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, M) or (N, M, P) :class:`numpy.ndarray` + Polar image to convert to cartesian 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. + 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` + + Returns + ------- + cartesianImage : (N, M) or (N, M, P) :class:`numpy.ndarray` + Cartesian image (3D cartesian image if 3D input image is given) + + See Also + -------- + :meth:`convertToCartesianImage` + """ + image, ptSettings = convertToCartesianImage(image, order=order, border=border, borderVal=borderVal, + useMultiThreading=useMultiThreading, settings=self) + return image + + def getPolarPointsImage(self, points): + """Convert list of cartesian points from image to polar image points based on transform metadata + + .. note:: + This does **not** convert from cartesian to polar points, but rather converts pixels from cartesian image to + pixels from polar image using :class:`ImageTransform`. + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + points : (N, 2) or (2,) :class:`numpy.ndarray` + List of cartesian points to convert to polar domain + + First column is x and second column is y + + Returns + ------- + polarPoints : (N, 2) or (2,) :class:`numpy.ndarray` + Corresponding polar points from cartesian :obj:`points` using :class:`ImageTransform` + + See Also + -------- + :meth:`getPolarPointsImage`, :meth:`getPolarPoints`, :meth:`getPolarPoints2` + """ + + return getPolarPointsImage(points, self) + + def getCartesianPointsImage(self, points): + """Convert list of polar points from image to cartesian image points based on transform metadata + + .. note:: + This does **not** convert from polar to cartesian points, but rather converts pixels from polar image to + pixels from cartesian image using :class:`ImageTransform`. + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + points : (N, 2) or (2,) :class:`numpy.ndarray` + List of polar points to convert to cartesian domain + + First column is r and second column is theta + + Returns + ------- + cartesianPoints : (N, 2) or (2,) :class:`numpy.ndarray` + Corresponding cartesian points from polar :obj:`points` using :class:`ImageTransform` + + See Also + -------- + :meth:`getCartesianPointsImage`, :meth:`getCartesianPoints`, :meth:`getCartesianPoints2` + """ + return getCartesianPointsImage(points, self) + + def __repr__(self): + return 'ImageTransform(center=%s, initialRadius=%i, finalRadius=%i, initialAngle=%f, finalAngle=%f, ' \ + 'cartesianImageSize=%s, polarImageSize=%s)' % (self.center, self.initialRadius, self.finalRadius, + self.initialAngle, self.finalAngle, + self.cartesianImageSize, self.polarImageSize) + + def __str__(self): + return self.__repr__() + +# Bypasses issue with ImageTransform not being defined for cyclic imports +# The answer is to include imports at the end so that everything is already defined before you import anything else +from polarTransform.convertToCartesianImage import convertToCartesianImage +from polarTransform.convertToPolarImage import convertToPolarImage +from polarTransform.pointsConversion import getCartesianPointsImage, getPolarPointsImage diff --git a/polarTransform/pointsConversion.py b/polarTransform/pointsConversion.py new file mode 100644 index 0000000..6c761ba --- /dev/null +++ b/polarTransform/pointsConversion.py @@ -0,0 +1,303 @@ +import numpy as np + + +def getCartesianPoints(rTheta, center): + """Convert list of polar points to cartesian points + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + rTheta : (N, 2) or (2,) :class:`numpy.ndarray` + List of cartesian points to convert to polar domain + + First column is r and second column is theta + center : (2,) :class:`numpy.ndarray` + Center to use for conversion to cartesian domain of polar points + + Format of center is (x, y) + + Returns + ------- + cartesianPoints : (N, 2) :class:`numpy.ndarray` + Corresponding cartesian points from cartesian :obj:`rTheta` + + First column is x and second column is y + + See Also + -------- + :meth:`getCartesianPoints2` + """ + if rTheta.ndim == 2: + x = rTheta[:, 0] * np.cos(rTheta[:, 1]) + center[0] + y = rTheta[:, 0] * np.sin(rTheta[:, 1]) + center[1] + else: + x = rTheta[0] * np.cos(rTheta[1]) + center[0] + y = rTheta[0] * np.sin(rTheta[1]) + center[1] + + return np.array([x, y]).T + + +def getCartesianPoints2(r, theta, center): + """Convert list of polar points to cartesian points + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + r : (N,) :class:`numpy.ndarray` + List of polar r points to convert to cartesian domain + theta : (N,) :class:`numpy.ndarray` + List of polar theta points to convert to cartesian domain + center : (2,) :class:`numpy.ndarray` + Center to use for conversion to cartesian domain of polar points + + Format of center is (x, y) + + Returns + ------- + x : (N,) :class:`numpy.ndarray` + Corresponding x points from polar :obj:`r` and :obj:`theta` + y : (N,) :class:`numpy.ndarray` + Corresponding y points from polar :obj:`r` and :obj:`theta` + + See Also + -------- + :meth:`getCartesianPoints` + """ + x = r * np.cos(theta) + center[0] + y = r * np.sin(theta) + center[1] + + return x, y + + +def getPolarPoints(xy, center): + """Convert list of cartesian points to polar points + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + xy : (N, 2) or (2,) :class:`numpy.ndarray` + List of cartesian points to convert to polar domain + + First column is x and second column is y + center : (2,) :class:`numpy.ndarray` + Center to use for conversion to polar domain of cartesian points + + Format of center is (x, y) + + Returns + ------- + polarPoints : (N, 2) :class:`numpy.ndarray` + Corresponding polar points from cartesian :obj:`xy` + + First column is r and second column is theta + + See Also + -------- + :meth:`getPolarPoints2` + """ + if xy.ndim == 2: + cX, cY = xy[:, 0] - center[0], xy[:, 1] - center[1] + else: + cX, cY = xy[0] - center[0], xy[1] - center[1] + + r = np.sqrt(cX ** 2 + cY ** 2) + theta = np.arctan2(cY, cX) + + # Make range of theta 0 -> 2pi instead of -pi -> pi + # According to StackOverflow, this is the fastest method: + # https://stackoverflow.com/questions/37358016/numpy-converting-range-of-angles-from-pi-pi-to-0-2pi + theta = np.where(theta < 0, theta + 2 * np.pi, theta) + + return np.array([r, theta]).T + + +def getPolarPoints2(x, y, center): + """Convert list of cartesian points to polar points + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + x : (N,) :class:`numpy.ndarray` + List of cartesian x points to convert to polar domain + y : (N,) :class:`numpy.ndarray` + List of cartesian y points to convert to polar domain + center : (2,) :class:`numpy.ndarray` + Center to use for conversion to polar domain of cartesian points + + Format of center is (x, y) + + Returns + ------- + r : (N,) :class:`numpy.ndarray` + Corresponding radii points from cartesian :obj:`x` and :obj:`y` + theta : (N,) :class:`numpy.ndarray` + Corresponding theta points from cartesian :obj:`x` and :obj:`y` + + See Also + -------- + :meth:`getPolarPoints` + """ + cX, cY = x - center[0], y - center[1] + + r = np.sqrt(cX ** 2 + cY ** 2) + + theta = np.arctan2(cY, cX) + + # Make range of theta 0 -> 2pi instead of -pi -> pi + # According to StackOverflow, this is the fastest method: + # https://stackoverflow.com/questions/37358016/numpy-converting-range-of-angles-from-pi-pi-to-0-2pi + theta = np.where(theta < 0, theta + 2 * np.pi, theta) + + return r, theta + + +def getPolarPointsImage(points, settings): + """Convert list of cartesian points from image to polar image points based on transform metadata + + .. warning:: + Cleaner and more succinct to use :meth:`ImageTransform.getPolarPointsImage` + + .. note:: + This does **not** convert from cartesian to polar points, but rather converts pixels from cartesian image to + pixels from polar image using :class:`ImageTransform`. + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + points : (N, 2) or (2,) :class:`numpy.ndarray` + List of cartesian points to convert to polar domain + + First column is x and second column is y + settings : :class:`ImageTransform` + Contains metadata for conversion from polar to cartesian domain + + 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. + + Returns + ------- + polarPoints : (N, 2) or (2,) :class:`numpy.ndarray` + Corresponding polar points from cartesian :obj:`points` using :obj:`settings` + + See Also + -------- + :meth:`ImageTransform.getPolarPointsImage`, :meth:`getPolarPoints`, :meth:`getPolarPoints2` + """ + # Convert points to NumPy array + points = np.asanyarray(points) + + # If there is only one point specified and number of dimensions is only one, then make the array a 1x2 array so that + # points[:, 0/1] will not throw an error + if points.ndim == 1 and points.shape[0] == 2: + points = np.expand_dims(points, axis=0) + needSqueeze = True + else: + needSqueeze = False + + # This is used to scale the result of the radius to get the appropriate Cartesian value + scaleRadius = settings.polarImageSize[0] / (settings.finalRadius - settings.initialRadius) + + # This is used to scale the result of the angle to get the appropriate Cartesian value + scaleAngle = settings.polarImageSize[1] / (settings.finalAngle - settings.initialAngle) + + # Take cartesian grid and convert to polar coordinates + polarPoints = getPolarPoints(points, settings.center) + + # Offset the radius by the initial source radius + polarPoints[:, 0] = polarPoints[:, 0] - 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 - initialAngle + 2 * np.pi, 2 * np.pi) + polarPoints[:, 1] = np.mod(polarPoints[:, 1] - settings.initialAngle + 2 * np.pi, 2 * np.pi) + + # Scale the radius using scale factor + # Scale the angle from radians to pixels using scale factor + polarPoints = polarPoints * [scaleRadius, scaleAngle] + + if needSqueeze: + return np.squeeze(polarPoints) + else: + return polarPoints + + +def getCartesianPointsImage(points, settings): + """Convert list of polar points from image to cartesian image points based on transform metadata + + .. warning:: + Cleaner and more succinct to use :meth:`ImageTransform.getCartesianPointsImage` + + .. note:: + This does **not** convert from polar to cartesian points, but rather converts pixels from polar image to + pixels from cartesian image using :class:`ImageTransform`. + + The returned points are not rounded to the nearest point. User must do that by hand if desired. + + Parameters + ---------- + points : (N, 2) or (2,) :class:`numpy.ndarray` + List of polar points to convert to cartesian domain + + First column is r and second column is theta + settings : :class:`ImageTransform` + Contains metadata for conversion from polar to cartesian domain + + 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. + + Returns + ------- + cartesianPoints : (N, 2) or (2,) :class:`numpy.ndarray` + Corresponding cartesian points from polar :obj:`points` using :obj:`settings` + + See Also + -------- + :meth:`ImageTransform.getCartesianPointsImage`, :meth:`getCartesianPoints`, :meth:`getCartesianPoints2` + """ + # Convert points to NumPy array + points = np.asanyarray(points) + + # If there is only one point specified and number of dimensions is only one, then make the array a 1x2 array so that + # points[:, 0/1] will not throw an error + if points.ndim == 1 and points.shape[0] == 2: + points = np.expand_dims(points, axis=0) + needSqueeze = True + else: + needSqueeze = False + + # This is used to scale the result of the radius to get the appropriate Cartesian value + scaleRadius = settings.polarImageSize[0] / (settings.finalRadius - settings.initialRadius) + + # This is used to scale the result of the angle to get the appropriate Cartesian value + scaleAngle = settings.polarImageSize[1] / (settings.finalAngle - settings.initialAngle) + + # Create a new copy of the points variable because we are going to change it and don't want the points parameter to + # change outside of this function + points = points.copy() + + # Scale the radius using scale factor + # Scale the angle from radians to pixels using scale factor + points = points / [scaleRadius, scaleAngle] + + # Offset the radius by the initial source radius + points[:, 0] = points[:, 0] + 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 - initialAngle + 2 * np.pi, 2 * np.pi) + points[:, 1] = np.mod(points[:, 1] + settings.initialAngle + 2 * np.pi, 2 * np.pi) + + # Take cartesian grid and convert to polar coordinates + cartesianPoints = getCartesianPoints(points, settings.center) + + if needSqueeze: + return np.squeeze(cartesianPoints) + else: + return cartesianPoints diff --git a/polarTransform/tests/__init__.py b/polarTransform/tests/__init__.py new file mode 100644 index 0000000..a08d414 --- /dev/null +++ b/polarTransform/tests/__init__.py @@ -0,0 +1,2 @@ +# __init__.py +# Required for find_packages to retrieve the test Python files \ No newline at end of file diff --git a/polarTransform/tests/data/horizontalLines.png b/polarTransform/tests/data/horizontalLines.png new file mode 100644 index 0000000..a69cba3 Binary files /dev/null and b/polarTransform/tests/data/horizontalLines.png differ diff --git a/polarTransform/tests/data/horizontalLinesAnimated.avi b/polarTransform/tests/data/horizontalLinesAnimated.avi new file mode 100644 index 0000000..5fef597 Binary files /dev/null and b/polarTransform/tests/data/horizontalLinesAnimated.avi differ diff --git a/polarTransform/tests/data/horizontalLinesAnimatedPolar.avi b/polarTransform/tests/data/horizontalLinesAnimatedPolar.avi new file mode 100644 index 0000000..f68c610 Binary files /dev/null and b/polarTransform/tests/data/horizontalLinesAnimatedPolar.avi differ diff --git a/polarTransform/tests/data/horizontalLinesPolarImage.png b/polarTransform/tests/data/horizontalLinesPolarImage.png new file mode 100644 index 0000000..18603e5 Binary files /dev/null and b/polarTransform/tests/data/horizontalLinesPolarImage.png differ diff --git a/tests/data/shortAxisApex.png b/polarTransform/tests/data/shortAxisApex.png similarity index 100% rename from tests/data/shortAxisApex.png rename to polarTransform/tests/data/shortAxisApex.png diff --git a/tests/data/shortAxisApexPolarImage.png b/polarTransform/tests/data/shortAxisApexPolarImage.png similarity index 100% rename from tests/data/shortAxisApexPolarImage.png rename to polarTransform/tests/data/shortAxisApexPolarImage.png diff --git a/tests/data/shortAxisApexPolarImage_centerMiddle.png b/polarTransform/tests/data/shortAxisApexPolarImage_centerMiddle.png similarity index 100% rename from tests/data/shortAxisApexPolarImage_centerMiddle.png rename to polarTransform/tests/data/shortAxisApexPolarImage_centerMiddle.png diff --git a/tests/data/verticalLines.png b/polarTransform/tests/data/verticalLines.png similarity index 100% rename from tests/data/verticalLines.png rename to polarTransform/tests/data/verticalLines.png diff --git a/polarTransform/tests/data/verticalLinesAnimated.avi b/polarTransform/tests/data/verticalLinesAnimated.avi new file mode 100644 index 0000000..26c8e46 Binary files /dev/null and b/polarTransform/tests/data/verticalLinesAnimated.avi differ diff --git a/polarTransform/tests/data/verticalLinesAnimatedPolar.avi b/polarTransform/tests/data/verticalLinesAnimatedPolar.avi new file mode 100644 index 0000000..206c547 Binary files /dev/null and b/polarTransform/tests/data/verticalLinesAnimatedPolar.avi differ diff --git a/tests/data/verticalLinesCartesianImageBorders2.png b/polarTransform/tests/data/verticalLinesCartesianImageBorders2.png similarity index 100% rename from tests/data/verticalLinesCartesianImageBorders2.png rename to polarTransform/tests/data/verticalLinesCartesianImageBorders2.png diff --git a/tests/data/verticalLinesCartesianImageBorders4.png b/polarTransform/tests/data/verticalLinesCartesianImageBorders4.png similarity index 100% rename from tests/data/verticalLinesCartesianImageBorders4.png rename to polarTransform/tests/data/verticalLinesCartesianImageBorders4.png diff --git a/tests/data/verticalLinesCartesianImage_scaled.png b/polarTransform/tests/data/verticalLinesCartesianImage_scaled.png similarity index 100% rename from tests/data/verticalLinesCartesianImage_scaled.png rename to polarTransform/tests/data/verticalLinesCartesianImage_scaled.png diff --git a/tests/data/verticalLinesCartesianImage_scaled2.png b/polarTransform/tests/data/verticalLinesCartesianImage_scaled2.png similarity index 100% rename from tests/data/verticalLinesCartesianImage_scaled2.png rename to polarTransform/tests/data/verticalLinesCartesianImage_scaled2.png diff --git a/tests/data/verticalLinesCartesianImage_scaled3.png b/polarTransform/tests/data/verticalLinesCartesianImage_scaled3.png similarity index 100% rename from tests/data/verticalLinesCartesianImage_scaled3.png rename to polarTransform/tests/data/verticalLinesCartesianImage_scaled3.png diff --git a/tests/data/verticalLinesPolarImage.png b/polarTransform/tests/data/verticalLinesPolarImage.png similarity index 100% rename from tests/data/verticalLinesPolarImage.png rename to polarTransform/tests/data/verticalLinesPolarImage.png diff --git a/tests/data/verticalLinesPolarImageBorders.png b/polarTransform/tests/data/verticalLinesPolarImageBorders.png similarity index 100% rename from tests/data/verticalLinesPolarImageBorders.png rename to polarTransform/tests/data/verticalLinesPolarImageBorders.png diff --git a/tests/data/verticalLinesPolarImageBorders3.png b/polarTransform/tests/data/verticalLinesPolarImageBorders3.png similarity index 100% rename from tests/data/verticalLinesPolarImageBorders3.png rename to polarTransform/tests/data/verticalLinesPolarImageBorders3.png diff --git a/tests/data/verticalLinesPolarImage_scaled.png b/polarTransform/tests/data/verticalLinesPolarImage_scaled.png similarity index 100% rename from tests/data/verticalLinesPolarImage_scaled.png rename to polarTransform/tests/data/verticalLinesPolarImage_scaled.png diff --git a/tests/data/verticalLinesPolarImage_scaled2.png b/polarTransform/tests/data/verticalLinesPolarImage_scaled2.png similarity index 100% rename from tests/data/verticalLinesPolarImage_scaled2.png rename to polarTransform/tests/data/verticalLinesPolarImage_scaled2.png diff --git a/tests/data/verticalLinesPolarImage_scaled3.png b/polarTransform/tests/data/verticalLinesPolarImage_scaled3.png similarity index 100% rename from tests/data/verticalLinesPolarImage_scaled3.png rename to polarTransform/tests/data/verticalLinesPolarImage_scaled3.png diff --git a/tests/testsGeneration.py b/polarTransform/tests/generateTests.py similarity index 61% rename from tests/testsGeneration.py rename to polarTransform/tests/generateTests.py index 481b0e4..56c732d 100644 --- a/tests/testsGeneration.py +++ b/polarTransform/tests/generateTests.py @@ -1,11 +1,23 @@ -import numpy as np +import os +import sys + +# Required specifically in each module so that searches happen at the parent directory for importing modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + import polarTransform -from .util import loadImage, saveImage +from polarTransform.tests.util import * + +# This file should only be ran to generate images contained in the data folder. Since much of this library is performing +# actions on images, the best way to validate through tests that it is working correctly, is to generate images using +# the library and then visually inspecting that the image looks correct. +# +# These images are uploaded and are apart of the repository itself so most of these images will not need to be +# regenerated unless a breaking change is made to the code that changes the output. +# Load input images that are used to generate output images shortAxisApexImage = loadImage('shortAxisApex.png') verticalLinesImage = loadImage('verticalLines.png') -horizontalLinesImage = loadImage('horizontalLines.png') -checkerboardImage = loadImage('checkerboard.png') +horizontalLines = loadImage('horizontalLines.png', convertToGrayscale=True) shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') @@ -16,7 +28,11 @@ verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') +verticalLinesAnimated = loadVideo('verticalLinesAnimated.avi') +horizontalLinesAnimated = loadVideo('horizontalLinesAnimated.avi') + +# Generate functions def generateShortAxisPolar(): polarImage, ptSettings = polarTransform.convertToPolarImage(shortAxisApexImage, center=[401, 365]) saveImage('shortAxisApexPolarImage.png', polarImage) @@ -52,12 +68,6 @@ def generateVerticalLinesPolar4(): saveImage('verticalLinesPolarImage_scaled3.png', polarImage) -def generateVerticalLinesCartesian(): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage, center=[128, 128], - imageSize=[256, 256], finalRadius=182) - saveImage('verticalLinesCartesianImage.png', cartesianImage) - - def generateVerticalLinesCartesian2(): cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage_scaled, initialRadius=30, finalRadius=100, @@ -70,32 +80,19 @@ def generateVerticalLinesCartesian2(): def generateVerticalLinesCartesian3(): cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage_scaled2, center=[128, 128], imageSize=[256, 256], - initialRadius=30, - finalRadius=100) + initialRadius=30, finalRadius=100) saveImage('verticalLinesCartesianImage_scaled2.png', cartesianImage) def generateVerticalLinesCartesian4(): cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage_scaled3, - initialRadius=30, - finalRadius=100, initialAngle=2 / 4 * np.pi, + initialRadius=30, finalRadius=100, + initialAngle=2 / 4 * np.pi, finalAngle=5 / 4 * np.pi, center=[128, 128], imageSize=[256, 256]) saveImage('verticalLinesCartesianImage_scaled3.png', cartesianImage) -def generateShortAxisApexCartesian(): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(shortAxisApexPolarImage, center=[401, 365], - imageSize=[608, 800], finalRadius=543) - saveImage('shortAxisApexCartesianImage.png', cartesianImage) - - -def generateShortAxisApexCartesian2(): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(shortAxisApexPolarImage_centerMiddle, - imageSize=[608, 800], finalRadius=503) - saveImage('shortAxisApexCartesianImage2.png', cartesianImage) - - def generateVerticalLinesBorders(): polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, border='constant', borderVal=128.0) saveImage('verticalLinesPolarImageBorders.png', polarImage) @@ -115,8 +112,73 @@ def generateVerticalLinesBorders2(): cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='nearest') saveImage('verticalLinesCartesianImageBorders4.png', cartesianImage) + +def generateHorizontalLinesPolar(): + polarImage, ptSettings = polarTransform.convertToPolarImage(horizontalLines) + saveImage('horizontalLinesPolarImage.png', polarImage) + + +def generateVerticalLinesAnimated(): + frameSize = 40 + + frames = [np.roll(verticalLinesImage, 12 * x, axis=1) for x in range(frameSize)] + image3D = np.stack(frames, axis=-1) + + saveVideo('verticalLinesAnimated.avi', image3D) + + +def generateVerticalLinesAnimatedPolar(): + frameSize = 40 + ptSettings = None + polarFrames = [] + + for x in range(frameSize): + frame = verticalLinesAnimated[..., x] + + # Call convert to polar image on each frame, uses the assumption that individual 2D image works fine based on + # other tests + if ptSettings: + polarFrame = ptSettings.convertToPolarImage(frame) + else: + polarFrame, ptSettings = polarTransform.convertToPolarImage(frame) + + polarFrames.append(polarFrame) + + polarImage3D = np.stack(polarFrames, axis=-1) + saveVideo('verticalLinesAnimatedPolar.avi', polarImage3D) + + +def generateHorizontalLinesAnimated(): + frameSize = 40 + + frames = [np.roll(horizontalLines, 36 * x, axis=0) for x in range(frameSize)] + image3D = np.stack(frames, axis=-1) + + saveVideo('horizontalLinesAnimated.avi', image3D) + + +def generateHorizontalLinesAnimatedPolar(): + frameSize = 40 + ptSettings = None + polarFrames = [] + + for x in range(frameSize): + frame = horizontalLinesAnimated[..., x] + + # Call convert to polar image on each frame, uses the assumption that individual 2D image works fine based on + # other tests + if ptSettings: + polarFrame = ptSettings.convertToPolarImage(frame) + else: + polarFrame, ptSettings = polarTransform.convertToPolarImage(frame) + + polarFrames.append(polarFrame) + + polarImage3D = np.stack(polarFrames, axis=-1) + saveVideo('horizontalLinesAnimatedPolar.avi', polarImage3D) + # Enable these functions as you see fit to generate the images -# Note: It is up to the developer to ensure these images are created and look like they are supposed to +# Note: It is up to the developer to visually inspect the output images that are created. # generateShortAxisPolar() # generateShortAxisPolar2() # generateVerticalLinesPolar() @@ -124,21 +186,17 @@ def generateVerticalLinesBorders2(): # generateVerticalLinesPolar3() # generateVerticalLinesPolar4() -# generateVerticalLinesCartesian() # generateVerticalLinesCartesian2() # generateVerticalLinesCartesian3() # generateVerticalLinesCartesian4() -# generateShortAxisApexCartesian() -# generateShortAxisApexCartesian2() - # generateVerticalLinesBorders() # generateVerticalLinesBorders2() -# TODO Rerun tests and correct for adjusted radiusSize -# TODO Add method support -# TODO Add note about origin and stuff (should I do that)? -# TODO Check origin -# TODO Add note about angle size and radius size -# TODO Explain order (0-5) -# TODO Add note in docs that cartesianImageSize and polarImageSize only contain first 2 dimensions +# generateHorizontalLinesPolar() + +# generateVerticalLinesAnimated() +# generateVerticalLinesAnimatedPolar() + +# generateHorizontalLinesAnimated() +# generateHorizontalLinesAnimatedPolar() diff --git a/polarTransform/tests/test_cartesianConversion.py b/polarTransform/tests/test_cartesianConversion.py new file mode 100644 index 0000000..ee11316 --- /dev/null +++ b/polarTransform/tests/test_cartesianConversion.py @@ -0,0 +1,281 @@ +import os +import sys +import unittest + +# Required specifically in each module so that searches happen at the parent directory for importing modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +import polarTransform +from polarTransform.tests.util import * + + +class TestCartesianConversion(unittest.TestCase): + def setUp(self): + self.shortAxisApexImage = loadImage('shortAxisApex.png') + self.verticalLinesImage = loadImage('verticalLines.png') + self.horizontalLinesImage = loadImage('horizontalLines.png', convertToGrayscale=True) + + self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') + self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') + self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') + self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') + self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') + self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') + + self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') + self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') + self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') + + self.horizontalLinesPolarImage = loadImage('horizontalLinesPolarImage.png', convertToGrayscale=True) + + self.verticalLinesAnimated = loadVideo('verticalLinesAnimated.avi') + self.verticalLinesAnimatedPolar = loadVideo('verticalLinesAnimatedPolar.avi') + self.horizontalLinesAnimated = loadVideo('horizontalLinesAnimated.avi', convertToGrayscale=True) + self.horizontalLinesAnimatedPolar = loadVideo('horizontalLinesAnimatedPolar.avi', convertToGrayscale=True) + + def test_defaultCenter(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage, + center=(401, 365), imageSize=(608, 800), + finalRadius=543) + + np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 543) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) + self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.shortAxisApexImage, 5) + + def test_notNumpyArrayCenter(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage_centerMiddle, + imageSize=(608, 800), finalRadius=503) + + np.testing.assert_array_equal(ptSettings.center, np.array([400, 304])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 503) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) + self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage_centerMiddle.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.shortAxisApexImage, 5) + + def test_RGBA(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage, + center=(128, 128), + imageSize=(256, 256), finalRadius=182) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 182) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.verticalLinesImage, 5) + + def test_IFRadius(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, + center=(128, 128), imageSize=(256, 256), + initialRadius=30, + finalRadius=100) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled2.shape[0:2]) + + np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled2) + + def test_IFRadiusAngle(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled3, + initialRadius=30, + finalRadius=100, initialAngle=2 / 4 * np.pi, + finalAngle=5 / 4 * np.pi, center=(128, 128), + imageSize=(256, 256)) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) + self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled3.shape[0:2]) + + np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled3) + + def test_IFRadiusAngleScaled(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, + initialRadius=30, finalRadius=100, + initialAngle=2 / 4 * np.pi, + finalAngle=5 / 4 * np.pi, + imageSize=(256, 256), center=(128, 128)) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) + self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled.shape[0:2]) + + np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled) + + def test_settings(self): + cartesianImage1, ptSettings1 = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, + initialRadius=30, finalRadius=100, + initialAngle=2 / 4 * np.pi, + finalAngle=5 / 4 * np.pi, + imageSize=(256, 256), center=(128, 128)) + + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, + settings=ptSettings1) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) + self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled.shape[0:2]) + + np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled) + + def test_centerOrientationsWithImageSize(self): + orientations = [ + ('bottom-left', np.array([0, 0]), [0, 128], [0, 128], [128, 256], [128, 256]), + ('bottom-middle', np.array([128, 0]), [0, 128], [0, 256], [128, 256], [0, 256]), + ('bottom-right', np.array([256, 0]), [0, 128], [128, 256], [128, 256], [0, 128]), + + ('middle-left', np.array([0, 128]), [0, 256], [0, 128], [0, 256], [128, 256]), + ('middle-middle', np.array([128, 128]), [0, 256], [0, 256], [0, 256], [0, 256]), + ('middle-right', np.array([256, 128]), [0, 256], [128, 256], [0, 256], [0, 128]), + + ('top-left', np.array([0, 256]), [128, 256], [0, 128], [0, 128], [128, 256]), + ('top-middle', np.array([128, 256]), [128, 256], [0, 256], [0, 128], [0, 256]), + ('top-right', np.array([256, 256]), [128, 256], [128, 256], [0, 128], [0, 128]) + ] + + for row in orientations: + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, + center=row[0], imageSize=(256, 256), + initialRadius=30, finalRadius=100) + + np.testing.assert_array_equal(ptSettings.center, row[1]) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + + np.testing.assert_almost_equal(cartesianImage[row[2][0]:row[2][1], row[3][0]:row[3][1], :], + self.verticalLinesCartesianImage_scaled2[row[4][0]:row[4][1], + row[5][0]:row[5][1], :]) + + def test_centerOrientationsWithoutImageSize(self): + orientations = [ + ('bottom-left', (100, 100), np.array([0, 0]), [128, 228], [128, 228]), + ('bottom-middle', (100, 200), np.array([100, 0]), [128, 228], [28, 228]), + ('bottom-right', (100, 100), np.array([100, 0]), [128, 228], [28, 128]), + + ('middle-left', (200, 100), np.array([0, 100]), [28, 228], [128, 228]), + ('middle-middle', (200, 200), np.array([100, 100]), [28, 228], [28, 228]), + ('middle-right', (200, 100), np.array([100, 100]), [28, 228], [28, 128]), + + ('top-left', (100, 100), np.array([0, 100]), [28, 128], [128, 228]), + ('top-middle', (100, 200), np.array([100, 100]), [28, 128], [28, 228]), + ('top-right', (100, 100), np.array([100, 100]), [28, 128], [28, 128]) + ] + + for row in orientations: + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, + center=row[0], initialRadius=30, + finalRadius=100) + + self.assertEqual(ptSettings.cartesianImageSize, row[1]) + np.testing.assert_array_equal(ptSettings.center, row[2]) + + np.testing.assert_almost_equal(cartesianImage, + self.verticalLinesCartesianImage_scaled2[row[3][0]:row[3][1], + row[4][0]:row[4][1], :]) + + def test_default_horizontalLines(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.horizontalLinesPolarImage, + center=(512, 384), imageSize=(768, 1024), + finalRadius=640) + + np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 640) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (768, 1024)) + self.assertEqual(ptSettings.polarImageSize, self.horizontalLinesPolarImage.shape) + + assert_image_approx_equal_average(cartesianImage, self.horizontalLinesImage, 5) + + def test_3d_support_rgb(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesAnimatedPolar, + center=(128, 128), imageSize=(256, 256), + finalRadius=182) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 182) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesAnimatedPolar.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.verticalLinesAnimated, 5) + + def test_3d_support_rgb_multithreaded(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesAnimatedPolar, + center=(128, 128), imageSize=(256, 256), + finalRadius=182, useMultiThreading=True) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 182) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) + self.assertEqual(ptSettings.polarImageSize, self.verticalLinesAnimatedPolar.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.verticalLinesAnimated, 5) + + def test_3d_support_grayscale(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.horizontalLinesAnimatedPolar, + center=(512, 384), imageSize=(768, 1024), + finalRadius=640) + + np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 640) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (768, 1024)) + self.assertEqual(ptSettings.polarImageSize, self.horizontalLinesAnimatedPolar.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.horizontalLinesAnimated, 5) + + def test_3d_support_grayscale_multithreaded(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.horizontalLinesAnimatedPolar, + center=(512, 384), imageSize=(768, 1024), + finalRadius=640, useMultiThreading=True) + + np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 640) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (768, 1024)) + self.assertEqual(ptSettings.polarImageSize, self.horizontalLinesAnimatedPolar.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.horizontalLinesAnimated, 5) + + +if __name__ == '__main__': + unittest.main() diff --git a/polarTransform/tests/test_pointConversion.py b/polarTransform/tests/test_pointConversion.py new file mode 100644 index 0000000..f3b9501 --- /dev/null +++ b/polarTransform/tests/test_pointConversion.py @@ -0,0 +1,60 @@ +import os +import sys +import unittest + +# Required specifically in each module so that searches happen at the parent directory for importing modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +import polarTransform +from polarTransform.tests.util import * + + +class TestPointConversion(unittest.TestCase): + def setUp(self): + self.shortAxisApexImage = loadImage('shortAxisApex.png') + self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') + + def test_polarConversion(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, + center=np.array([401, 365])) + + np.testing.assert_array_equal(ptSettings.getPolarPointsImage([401, 365]), np.array([0, 0])) + np.testing.assert_array_equal(ptSettings.getPolarPointsImage([[401, 365], [401, 365]]), + np.array([[0, 0], [0, 0]])) + + np.testing.assert_array_equal(ptSettings.getPolarPointsImage((401, 365)), np.array([0, 0])) + np.testing.assert_array_equal(ptSettings.getPolarPointsImage(((401, 365), (401, 365))), + np.array([[0, 0], [0, 0]])) + + np.testing.assert_array_equal(ptSettings.getPolarPointsImage(np.array([401, 365])), np.array([0, 0])) + np.testing.assert_array_equal(ptSettings.getPolarPointsImage(np.array([[401, 365], [401, 365]])), + np.array([[0, 0], [0, 0]])) + + np.testing.assert_array_equal(ptSettings.getPolarPointsImage([[451, 365], [401, 400], [348, 365], [401, 305]]), + np.array([[50 * 802 / 543, 0], [35 * 802 / 543, 400], [53 * 802 / 543, 800], + [60 * 802 / 543, 1200]])) + + def test_cartesianConversion(self): + cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage, + center=[401, 365], imageSize=[608, 800], + finalRadius=543) + + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage([0, 0]), np.array([401, 365])) + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage([[0, 0], [0, 0]]), + np.array([[401, 365], [401, 365]])) + + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage((0, 0)), np.array([401, 365])) + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(((0, 0), (0, 0))), + np.array([[401, 365], [401, 365]])) + + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(np.array([0, 0])), np.array([401, 365])) + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(np.array([[0, 0], [0, 0]])), + np.array([[401, 365], [401, 365]])) + + np.testing.assert_array_equal(ptSettings.getCartesianPointsImage( + np.array([[50 * 802 / 543, 0], [35 * 802 / 543, 400], [53 * 802 / 543, 800], + [60 * 802 / 543, 1200]])), np.array([[451, 365], [401, 400], [348, 365], [401, 305]])) + + +if __name__ == '__main__': + unittest.main() diff --git a/polarTransform/tests/test_polarCartesianConversion.py b/polarTransform/tests/test_polarCartesianConversion.py new file mode 100644 index 0000000..a93c2b8 --- /dev/null +++ b/polarTransform/tests/test_polarCartesianConversion.py @@ -0,0 +1,93 @@ +import os +import sys +import unittest + +# Required specifically in each module so that searches happen at the parent directory for importing modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +import polarTransform +from polarTransform.tests.util import * + + +class TestPolarAndCartesianConversion(unittest.TestCase): + def setUp(self): + self.shortAxisApexImage = loadImage('shortAxisApex.png') + self.verticalLinesImage = loadImage('verticalLines.png') + + self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') + self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') + self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') + self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') + self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') + self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') + + self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') + self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') + self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') + + self.verticalLinesPolarImageBorders = loadImage('verticalLinesPolarImageBorders.png') + self.verticalLinesCartesianImageBorders2 = loadImage('verticalLinesCartesianImageBorders2.png') + self.verticalLinesPolarImageBorders3 = loadImage('verticalLinesPolarImageBorders3.png') + self.verticalLinesCartesianImageBorders4 = loadImage('verticalLinesCartesianImageBorders4.png') + + def test_default(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, + center=np.array([401, 365])) + + cartesianImage = ptSettings.convertToCartesianImage(polarImage) + + np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 543) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) + self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[0:2]) + + assert_image_approx_equal_average(cartesianImage, self.shortAxisApexImage, 5) + + def test_default2(self): + polarImage1, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, + center=np.array([401, 365]), radiusSize=2000, + angleSize=4000) + + cartesianImage = ptSettings.convertToCartesianImage(polarImage1) + ptSettings.polarImageSize = self.shortAxisApexPolarImage.shape[0:2] + polarImage = ptSettings.convertToPolarImage(cartesianImage) + + np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 543) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) + self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[0:2]) + + assert_image_equal(polarImage, self.shortAxisApexPolarImage, 10) + + def test_borders(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, border='constant', + borderVal=128.0) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImageBorders) + + ptSettings.cartesianImageSize = (500, 500) + ptSettings.center = np.array([250, 250]) + cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='constant', borderVal=255.0) + + np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImageBorders2) + + def test_borders2(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, border='nearest') + + np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImageBorders3) + + ptSettings.cartesianImageSize = (500, 500) + ptSettings.center = np.array([250, 250]) + cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='nearest') + + np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImageBorders4) + + +if __name__ == '__main__': + unittest.main() diff --git a/polarTransform/tests/test_polarConversion.py b/polarTransform/tests/test_polarConversion.py new file mode 100644 index 0000000..fb34a03 --- /dev/null +++ b/polarTransform/tests/test_polarConversion.py @@ -0,0 +1,216 @@ +import os +import sys +import unittest + +# Required specifically in each module so that searches happen at the parent directory for importing modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +import polarTransform +from polarTransform.tests.util import * + + +class TestPolarConversion(unittest.TestCase): + def setUp(self): + self.shortAxisApexImage = loadImage('shortAxisApex.png') + self.verticalLinesImage = loadImage('verticalLines.png') + self.horizontalLinesImage = loadImage('horizontalLines.png', convertToGrayscale=True) + + self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') + self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') + self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') + self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') + self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') + self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') + + self.horizontalLinesPolarImage = loadImage('horizontalLinesPolarImage.png', convertToGrayscale=True) + + self.verticalLinesAnimated = loadVideo('verticalLinesAnimated.avi') + self.verticalLinesAnimatedPolar = loadVideo('verticalLinesAnimatedPolar.avi') + self.horizontalLinesAnimated = loadVideo('horizontalLinesAnimated.avi', convertToGrayscale=True) + self.horizontalLinesAnimatedPolar = loadVideo('horizontalLinesAnimatedPolar.avi', convertToGrayscale=True) + + def test_default(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, + center=np.array([401, 365])) + + np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 543) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) + self.assertEqual(ptSettings.polarImageSize, (802, 1600)) + + np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) + + def test_final_radius(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, + center=np.array([350, 365])) + + np.testing.assert_array_equal(ptSettings.center, np.array([350, 365])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 580) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) + self.assertEqual(ptSettings.polarImageSize, (898, 1600)) + + def test_defaultCenter(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage) + + np.testing.assert_array_equal(ptSettings.center, np.array([400, 304])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 503) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) + self.assertEqual(ptSettings.polarImageSize, (800, 1600)) + + np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage_centerMiddle) + + def test_notNumpyArrayCenter(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, center=(401, 365)) + np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) + np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) + + polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, center=(401, 365)) + np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) + np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) + + def test_IFRadius(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, + finalRadius=100) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (99, 1024)) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled2) + + def test_IFRadiusAngle(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, + finalRadius=100, initialAngle=2 / 4 * np.pi, + finalAngle=5 / 4 * np.pi) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) + self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (99, 384)) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled3) + + def test_IFRadiusAngleScaled(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, + finalRadius=100, initialAngle=2 / 4 * np.pi, + finalAngle=5 / 4 * np.pi, radiusSize=140, + angleSize=700) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) + self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (140, 700)) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled) + + def test_settings(self): + polarImage1, ptSettings1 = polarTransform.convertToPolarImage(self.verticalLinesImage, + initialRadius=30, finalRadius=100, + initialAngle=2 / 4 * np.pi, + finalAngle=5 / 4 * np.pi, radiusSize=140, + angleSize=700) + + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, settings=ptSettings1) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 30) + self.assertEqual(ptSettings.finalRadius, 100) + self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) + self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (140, 700)) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled) + + polarImage2 = ptSettings1.convertToPolarImage(self.verticalLinesImage) + np.testing.assert_almost_equal(polarImage2, self.verticalLinesPolarImage_scaled) + + def test_default_horizontalLines(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.horizontalLinesImage) + + np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) + self.assertEqual(ptSettings.initialRadius, 0) + # sqrt(512^2 + 384^2) maximum distance = 640 + self.assertEqual(ptSettings.finalRadius, 640) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.horizontalLinesImage.shape) + self.assertEqual(ptSettings.polarImageSize, (1024, 2048)) + + np.testing.assert_almost_equal(polarImage, self.horizontalLinesPolarImage) + + def test_3d_support_rgb(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesAnimated) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 182) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesAnimated.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (256, 1024)) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesAnimatedPolar) + + def test_3d_support_rgb_multithreaded(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesAnimated, useMultiThreading=True) + + np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 182) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesAnimated.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (256, 1024)) + + np.testing.assert_almost_equal(polarImage, self.verticalLinesAnimatedPolar) + + def test_3d_support_grayscale(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.horizontalLinesAnimated) + + np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 640) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.horizontalLinesAnimated.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (1024, 2048)) + + np.testing.assert_almost_equal(polarImage, self.horizontalLinesAnimatedPolar) + + def test_3d_support_grayscale_multithreaded(self): + polarImage, ptSettings = polarTransform.convertToPolarImage(self.horizontalLinesAnimated, + useMultiThreading=True) + + np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) + self.assertEqual(ptSettings.initialRadius, 0) + self.assertEqual(ptSettings.finalRadius, 640) + self.assertEqual(ptSettings.initialAngle, 0.0) + self.assertEqual(ptSettings.finalAngle, 2 * np.pi) + self.assertEqual(ptSettings.cartesianImageSize, self.horizontalLinesAnimated.shape[0:2]) + self.assertEqual(ptSettings.polarImageSize, (1024, 2048)) + + np.testing.assert_almost_equal(polarImage, self.horizontalLinesAnimatedPolar) + + +if __name__ == '__main__': + unittest.main() diff --git a/polarTransform/tests/util.py b/polarTransform/tests/util.py new file mode 100644 index 0000000..ba5f724 --- /dev/null +++ b/polarTransform/tests/util.py @@ -0,0 +1,99 @@ +import os + +import cv2 +import imageio +import numpy as np + +dataDirectory = os.path.join(os.path.dirname(__file__), 'data') + + +def loadImage(filename, flipud=True, convertToGrayscale=False): + image = imageio.imread(os.path.join(dataDirectory, filename), ignoregamma=True) + + if convertToGrayscale and image.ndim == 3: + image = image[:, :, 0] + elif image.ndim == 3 and image.shape[-1] == 4: + image = image[:, :, 0:3] + + return np.flipud(image) if flipud else image + + +def loadVideo(filename, flipud=True, convertToGrayscale=False): + capture = cv2.VideoCapture(os.path.join(dataDirectory, filename)) + frames = [] + + while capture.isOpened(): + # Read frame + returnValue, frame = capture.read() + + # Error reading image, move on + if not returnValue: + break + + # Convert to grayscale or remove alpha component if RGB image + if convertToGrayscale and frame.ndim == 3: + frame = frame[:, :, 0] + elif frame.ndim == 3 and frame.shape[-1] == 4: + frame = frame[:, :, 0:3] + + frames.append(frame) + + # Combine the video on the last axis + image3D = np.stack(frames, axis=-1) + + if flipud: + image3D = np.flipud(image3D) + + return image3D + + +def saveImage(filename, image, flipud=True): + imageio.imwrite(os.path.join(dataDirectory, filename), np.flipud(image) if flipud else image) + + +def saveVideo(filename, image, fps=20, fourcc='MJLS', flipud=True): + # Assuming color is present if four dimensions are available + hasColor = image.ndim == 4 + + # Construct four character code + if isinstance(fourcc, str): + fourcc = cv2.VideoWriter_fourcc(*fourcc) + + # Construct codec to use and create writer + # MJLS is one of the few FFMPEG formats that supports lossy encoding in OpenCV specifically because it does not + # convert to YUV color space + writer = cv2.VideoWriter(os.path.join(dataDirectory, filename), fourcc, fps, image.shape[1::-1], isColor=hasColor) + + # Flip image if specified + if flipud: + image = np.flipud(image) + + # Write frames + for x in range(image.shape[-1]): + writer.write(image[..., x]) + + # Finish writing + writer.release() + + +def assert_image_equal(desired, actual, diff): + difference = np.abs(desired.astype(int) - actual.astype(int)).astype(np.uint8) + + assert (np.all(difference <= diff)) + + +def assert_image_approx_equal_average(desired, actual, averageDiff): + assert desired.ndim == actual.ndim, 'Images are not equal, difference in dimensions: %i != %i' % \ + (desired.ndim, actual.ndim) + assert desired.shape == actual.shape, 'Images are not equal, difference in shape %s != %s' % \ + (desired.shape, actual.shape) + + # Calculate the difference between the two images + difference = np.abs(desired.astype(int) - actual.astype(int)).astype(np.uint8) + + # Get average difference between each different pixel + averageDiffPerPixel = np.sum(difference, axis=(0, 1)) / np.sum(difference > 0, axis=(0, 1)) + + assert np.all(averageDiffPerPixel < averageDiff), 'Images are not equal, average difference between each channel ' \ + 'is not less than the given threshold, %s < %s' % \ + (averageDiffPerPixel, averageDiff) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..a7c53f6 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +opencv-python +sphinx-rtd-theme +codecov \ No newline at end of file diff --git a/setup.py b/setup.py index d11f333..dfa2a33 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ -from setuptools import setup import os +from setuptools import setup, find_packages + +from polarTransform._version import __version__ + currentPath = os.path.abspath(os.path.dirname(__file__)) # Get the long description from the README file @@ -9,9 +12,10 @@ long_description = '\n' + long_description setup(name='polarTransform', - version='1.0.1', - description='Library that can converts between polar and cartesian domain.', + version=__version__, + description='Library that can converts between polar and cartesian domain with images and individual points.', long_description=long_description, + long_description_content_type='text/x-rst', author='Addison Elliott', author_email='addison.elliott@gmail.com', url='https://github.com/addisonElliott/polarTransform', @@ -20,15 +24,19 @@ 'Topic :: Scientific/Engineering', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3' + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6' ], - keywords='polar transform cartesian conversion logPolar linearPolar cv2 opencv radius theta angle', + keywords='polar transform cartesian conversion logPolar linearPolar cv2 opencv radius theta angle image images', project_urls={ 'Documentation': 'http://polartransform.readthedocs.io', 'Source': 'https://github.com/addisonElliott/polarTransform', 'Tracker': 'https://github.com/addisonElliott/polarTransform/issues', }, python_requires='>=3', - py_modules=['polarTransform'], + packages=find_packages(), + include_package_data=True, license='MIT License', install_requires=[ 'numpy', 'scipy', 'scikit-image'] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 203562b..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# __init__.py \ No newline at end of file diff --git a/tests/data/shortAxisApexCartesianImage.png b/tests/data/shortAxisApexCartesianImage.png deleted file mode 100644 index 6e3e237..0000000 Binary files a/tests/data/shortAxisApexCartesianImage.png and /dev/null differ diff --git a/tests/data/shortAxisApexCartesianImage2.png b/tests/data/shortAxisApexCartesianImage2.png deleted file mode 100644 index 1b7311f..0000000 Binary files a/tests/data/shortAxisApexCartesianImage2.png and /dev/null differ diff --git a/tests/data/verticalLinesCartesianImage.png b/tests/data/verticalLinesCartesianImage.png deleted file mode 100644 index 16e1550..0000000 Binary files a/tests/data/verticalLinesCartesianImage.png and /dev/null differ diff --git a/tests/test_polarTransform.py b/tests/test_polarTransform.py deleted file mode 100644 index 885fd4e..0000000 --- a/tests/test_polarTransform.py +++ /dev/null @@ -1,503 +0,0 @@ -import os -import sys -import unittest - -import numpy as np - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from util import loadImage, assert_image_equal - -import polarTransform - - -class TestPolarConversion(unittest.TestCase): - def setUp(self): - self.shortAxisApexImage = loadImage('shortAxisApex.png') - self.verticalLinesImage = loadImage('verticalLines.png') - - self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') - self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') - self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') - self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') - self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') - self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') - - def test_default(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=np.array([401, 365])) - - np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 543) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) - self.assertEqual(ptSettings.polarImageSize, (802, 1600)) - - np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) - - def test_final_radius(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=np.array([350, 365])) - - np.testing.assert_array_equal(ptSettings.center, np.array([350, 365])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 580) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, - self.shortAxisApexImage.shape) - self.assertEqual(ptSettings.polarImageSize, (898, 1600)) - - def test_defaultCenter(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage) - - np.testing.assert_array_equal(ptSettings.center, np.array([400, 304])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 503) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) - self.assertEqual(ptSettings.polarImageSize, (800, 1600)) - - np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage_centerMiddle) - - def test_notNumpyArrayCenter(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=[401, 365]) - np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) - np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) - - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=(401, 365)) - np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) - np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) - - def test_RGBA(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 182) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) - self.assertEqual(ptSettings.polarImageSize, (256, 1024)) - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage) - - def test_IFRadius(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, - finalRadius=100) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) - self.assertEqual(ptSettings.polarImageSize, (99, 1024)) - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled2) - - def test_IFRadiusAngle(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, - finalRadius=100, initialAngle=2 / 4 * np.pi, - finalAngle=5 / 4 * np.pi) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) - self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) - self.assertEqual(ptSettings.polarImageSize, (99, 384)) - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled3) - - def test_IFRadiusAngleScaled(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, - finalRadius=100, initialAngle=2 / 4 * np.pi, - finalAngle=5 / 4 * np.pi, radiusSize=140, - angleSize=700) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) - self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) - self.assertEqual(ptSettings.polarImageSize, (140, 700)) - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled) - - def test_settings(self): - polarImage1, ptSettings1 = polarTransform.convertToPolarImage(self.verticalLinesImage, - initialRadius=30, - finalRadius=100, initialAngle=2 / 4 * np.pi, - finalAngle=5 / 4 * np.pi, radiusSize=140, - angleSize=700) - - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, settings=ptSettings1) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) - self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[0:2]) - self.assertEqual(ptSettings.polarImageSize, (140, 700)) - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled) - - polarImage2 = ptSettings1.convertToPolarImage(self.verticalLinesImage) - np.testing.assert_almost_equal(polarImage2, self.verticalLinesPolarImage_scaled) - - -class TestCartesianConversion(unittest.TestCase): - def setUp(self): - self.shortAxisApexImage = loadImage('shortAxisApex.png') - self.verticalLinesImage = loadImage('verticalLines.png') - - self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') - self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') - self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') - self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') - self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') - self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') - - self.verticalLinesCartesianImage = loadImage('verticalLinesCartesianImage.png') - self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') - self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') - self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') - - self.shortAxisApexCartesianImage = loadImage('shortAxisApexCartesianImage.png') - self.shortAxisApexCartesianImage2 = loadImage('shortAxisApexCartesianImage2.png') - - def test_defaultCenter(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage, - center=[401, 365], imageSize=[608, 800], - finalRadius=543) - - np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 543) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) - self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.shortAxisApexCartesianImage) - - def test_notNumpyArrayCenter(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage_centerMiddle, - imageSize=[608, 800], finalRadius=503) - - np.testing.assert_array_equal(ptSettings.center, np.array([400, 304])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 503) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) - self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage_centerMiddle.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.shortAxisApexCartesianImage2) - - def test_RGBA(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage, - center=[128, 128], - imageSize=[256, 256], finalRadius=182) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 182) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) - self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage) - - def test_IFRadius(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, - center=[128, 128], imageSize=[256, 256], - initialRadius=30, - finalRadius=100) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) - self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled2.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled2) - - def test_IFRadiusAngle(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled3, - initialRadius=30, - finalRadius=100, initialAngle=2 / 4 * np.pi, - finalAngle=5 / 4 * np.pi, center=[128, 128], - imageSize=[256, 256]) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) - self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) - self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled3.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled3) - - def test_IFRadiusAngleScaled(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, - initialRadius=30, finalRadius=100, - initialAngle=2 / 4 * np.pi, - finalAngle=5 / 4 * np.pi, - imageSize=[256, 256], - center=[128, 128]) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) - self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) - self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled) - - def test_settings(self): - cartesianImage1, ptSettings1 = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, - initialRadius=30, finalRadius=100, - initialAngle=2 / 4 * np.pi, - finalAngle=5 / 4 * np.pi, - imageSize=[256, 256], - center=[128, 128]) - - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, - settings=ptSettings1) - - np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) - self.assertEqual(ptSettings.initialRadius, 30) - self.assertEqual(ptSettings.finalRadius, 100) - self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) - self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) - self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled) - - def test_centerOrientationsWithImageSize(self): - orientations = [ - ('bottom-left', np.array([0, 0]), [0, 128], [0, 128], [128, 256], [128, 256]), - ('bottom-middle', np.array([128, 0]), [0, 128], [0, 256], [128, 256], [0, 256]), - ('bottom-right', np.array([256, 0]), [0, 128], [128, 256], [128, 256], [0, 128]), - - ('middle-left', np.array([0, 128]), [0, 256], [0, 128], [0, 256], [128, 256]), - ('middle-middle', np.array([128, 128]), [0, 256], [0, 256], [0, 256], [0, 256]), - ('middle-right', np.array([256, 128]), [0, 256], [128, 256], [0, 256], [0, 128]), - - ('top-left', np.array([0, 256]), [128, 256], [0, 128], [0, 128], [128, 256]), - ('top-middle', np.array([128, 256]), [128, 256], [0, 256], [0, 128], [0, 256]), - ('top-right', np.array([256, 256]), [128, 256], [128, 256], [0, 128], [0, 128]) - ] - - for row in orientations: - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, - center=row[0], - imageSize=[256, 256], - initialRadius=30, - finalRadius=100) - - np.testing.assert_array_equal(ptSettings.center, row[1]) - self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) - - np.testing.assert_almost_equal(cartesianImage[row[2][0]:row[2][1], row[3][0]:row[3][1], :], - self.verticalLinesCartesianImage_scaled2[row[4][0]:row[4][1], - row[5][0]:row[5][1], :]) - - def test_centerOrientationsWithoutImageSize(self): - orientations = [ - ('bottom-left', (100, 100), np.array([0, 0]), [128, 228], [128, 228]), - ('bottom-middle', (100, 200), np.array([100, 0]), [128, 228], [28, 228]), - ('bottom-right', (100, 100), np.array([100, 0]), [128, 228], [28, 128]), - - ('middle-left', (200, 100), np.array([0, 100]), [28, 228], [128, 228]), - ('middle-middle', (200, 200), np.array([100, 100]), [28, 228], [28, 228]), - ('middle-right', (200, 100), np.array([100, 100]), [28, 228], [28, 128]), - - ('top-left', (100, 100), np.array([0, 100]), [28, 128], [128, 228]), - ('top-middle', (100, 200), np.array([100, 100]), [28, 128], [28, 228]), - ('top-right', (100, 100), np.array([100, 100]), [28, 128], [28, 128]) - ] - - for row in orientations: - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, - center=row[0], - initialRadius=30, - finalRadius=100) - - self.assertEqual(ptSettings.cartesianImageSize, row[1]) - np.testing.assert_array_equal(ptSettings.center, row[2]) - - np.testing.assert_almost_equal(cartesianImage, - self.verticalLinesCartesianImage_scaled2[row[3][0]:row[3][1], - row[4][0]:row[4][1], :]) - - -class TestPolarAndCartesianConversion(unittest.TestCase): - def setUp(self): - self.shortAxisApexImage = loadImage('shortAxisApex.png') - self.verticalLinesImage = loadImage('verticalLines.png') - - self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') - self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') - self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') - self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') - self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') - self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') - - self.verticalLinesCartesianImage = loadImage('verticalLinesCartesianImage.png') - self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') - self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') - self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') - - self.shortAxisApexCartesianImage = loadImage('shortAxisApexCartesianImage.png') - self.shortAxisApexCartesianImage2 = loadImage('shortAxisApexCartesianImage2.png') - - self.verticalLinesPolarImageBorders = loadImage('verticalLinesPolarImageBorders.png') - self.verticalLinesCartesianImageBorders2 = loadImage('verticalLinesCartesianImageBorders2.png') - self.verticalLinesPolarImageBorders3 = loadImage('verticalLinesPolarImageBorders3.png') - self.verticalLinesCartesianImageBorders4 = loadImage('verticalLinesCartesianImageBorders4.png') - - def test_default(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=np.array([401, 365])) - - cartesianImage = ptSettings.convertToCartesianImage(polarImage) - - np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 543) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) - self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[0:2]) - - np.testing.assert_almost_equal(cartesianImage, self.shortAxisApexCartesianImage) - - def test_default2(self): - polarImage1, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=np.array([401, 365]), radiusSize=2000, - angleSize=4000) - - cartesianImage = ptSettings.convertToCartesianImage(polarImage1) - ptSettings.polarImageSize = self.shortAxisApexPolarImage.shape[0:2] - polarImage = ptSettings.convertToPolarImage(cartesianImage) - - np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) - self.assertEqual(ptSettings.initialRadius, 0) - self.assertEqual(ptSettings.finalRadius, 543) - self.assertEqual(ptSettings.initialAngle, 0.0) - self.assertEqual(ptSettings.finalAngle, 2 * np.pi) - self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) - self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[0:2]) - - assert_image_equal(polarImage, self.shortAxisApexPolarImage, 10) - - def test_borders(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, border='constant', - borderVal=128.0) - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImageBorders) - - ptSettings.cartesianImageSize = (500, 500) - ptSettings.center = np.array([250, 250]) - cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='constant', borderVal=255.0) - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImageBorders2) - - def test_borders2(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, border='nearest') - - np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImageBorders3) - - ptSettings.cartesianImageSize = (500, 500) - ptSettings.center = np.array([250, 250]) - cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='nearest') - - np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImageBorders4) - - -class TestPointConversion(unittest.TestCase): - def setUp(self): - self.shortAxisApexImage = loadImage('shortAxisApex.png') - self.verticalLinesImage = loadImage('verticalLines.png') - - self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') - self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') - self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') - self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') - self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') - self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') - - self.verticalLinesCartesianImage = loadImage('verticalLinesCartesianImage.png') - self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') - self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') - self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') - - self.shortAxisApexCartesianImage = loadImage('shortAxisApexCartesianImage.png') - self.shortAxisApexCartesianImage2 = loadImage('shortAxisApexCartesianImage2.png') - - self.verticalLinesPolarImageBorders = loadImage('verticalLinesPolarImageBorders.png') - self.verticalLinesCartesianImageBorders2 = loadImage('verticalLinesCartesianImageBorders2.png') - self.verticalLinesPolarImageBorders3 = loadImage('verticalLinesPolarImageBorders3.png') - self.verticalLinesCartesianImageBorders4 = loadImage('verticalLinesCartesianImageBorders4.png') - - def test_polarConversion(self): - polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, - center=np.array([401, 365])) - - np.testing.assert_array_equal(ptSettings.getPolarPointsImage([401, 365]), np.array([0, 0])) - np.testing.assert_array_equal(ptSettings.getPolarPointsImage([[401, 365], [401, 365]]), - np.array([[0, 0], [0, 0]])) - - np.testing.assert_array_equal(ptSettings.getPolarPointsImage((401, 365)), np.array([0, 0])) - np.testing.assert_array_equal(ptSettings.getPolarPointsImage(((401, 365), (401, 365))), - np.array([[0, 0], [0, 0]])) - - np.testing.assert_array_equal(ptSettings.getPolarPointsImage(np.array([401, 365])), np.array([0, 0])) - np.testing.assert_array_equal(ptSettings.getPolarPointsImage(np.array([[401, 365], [401, 365]])), - np.array([[0, 0], [0, 0]])) - - np.testing.assert_array_equal(ptSettings.getPolarPointsImage([[451, 365], [401, 400], [348, 365], [401, 305]]), - np.array([[50 * 802 / 543, 0], [35 * 802 / 543, 400], [53 * 802 / 543, 800], - [60 * 802 / 543, 1200]])) - - def test_cartesianConversion(self): - cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage, - center=[401, 365], imageSize=[608, 800], - finalRadius=543) - - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage([0, 0]), np.array([401, 365])) - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage([[0, 0], [0, 0]]), - np.array([[401, 365], [401, 365]])) - - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage((0, 0)), np.array([401, 365])) - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(((0, 0), (0, 0))), - np.array([[401, 365], [401, 365]])) - - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(np.array([0, 0])), np.array([401, 365])) - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(np.array([[0, 0], [0, 0]])), - np.array([[401, 365], [401, 365]])) - - np.testing.assert_array_equal(ptSettings.getCartesianPointsImage( - np.array([[50 * 802 / 543, 0], [35 * 802 / 543, 400], [53 * 802 / 543, 800], - [60 * 802 / 543, 1200]])), np.array([[451, 365], [401, 400], [348, 365], [401, 305]])) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/util.py b/tests/util.py deleted file mode 100644 index 16dbe5b..0000000 --- a/tests/util.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -import imageio -import numpy as np - -dataDirectory = os.path.join(os.path.dirname(__file__), 'data') - - -def loadImage(filename, flipud=True, convertToGrayscale=False): - image = imageio.imread(os.path.join(dataDirectory, filename), ignoregamma=True) - - if convertToGrayscale: - image = image[:, :, 0] - - return np.flipud(image) if flipud else image - - -def saveImage(filename, image, flipud=True): - imageio.imwrite(os.path.join(dataDirectory, filename), np.flipud(image) if flipud else image) - - -def assert_image_equal(desired, actual, diff): - difference = np.abs(desired.astype(int) - actual.astype(int)).astype(np.uint8) - - assert (np.all(difference <= diff))