├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── centerAnnotations.png │ ├── css │ │ └── custom.css │ ├── shortAxisApex.png │ ├── shortAxisApexPolarImage.png │ ├── verticalLines.png │ ├── verticalLinesCartesianImageBorders2.png │ ├── verticalLinesCartesianImage_scaled.png │ ├── verticalLinesCartesianImage_scaled2.png │ ├── verticalLinesPolarImage.png │ ├── verticalLinesPolarImageBorders.png │ ├── verticalLinesPolarImageBorders3.png │ ├── verticalLinesPolarImage_scaled.png │ ├── verticalLinesPolarImage_scaled2.png │ └── verticalLinesPolarImage_scaled3.png │ ├── conf.py │ ├── getting-started.rst │ ├── index.rst │ ├── polarTransform.rst │ └── user-guide.rst ├── polarTransform ├── __init__.py ├── _version.py ├── convertToCartesianImage.py ├── convertToPolarImage.py ├── imageTransform.py ├── pointsConversion.py └── tests │ ├── __init__.py │ ├── data │ ├── horizontalLines.png │ ├── horizontalLinesAnimated.avi │ ├── horizontalLinesAnimatedPolar.avi │ ├── horizontalLinesPolarImage.png │ ├── shortAxisApex.png │ ├── shortAxisApexPolarImage.png │ ├── shortAxisApexPolarImage_centerMiddle.png │ ├── verticalLines.png │ ├── verticalLinesAnimated.avi │ ├── verticalLinesAnimatedPolar.avi │ ├── verticalLinesCartesianImageBorders2.png │ ├── verticalLinesCartesianImageBorders4.png │ ├── verticalLinesCartesianImage_scaled.png │ ├── verticalLinesCartesianImage_scaled2.png │ ├── verticalLinesCartesianImage_scaled3.png │ ├── verticalLinesPolarImage.png │ ├── verticalLinesPolarImageBorders.png │ ├── verticalLinesPolarImageBorders3.png │ ├── verticalLinesPolarImage_scaled.png │ ├── verticalLinesPolarImage_scaled2.png │ └── verticalLinesPolarImage_scaled3.png │ ├── generateTests.py │ ├── test_cartesianConversion.py │ ├── test_pointConversion.py │ ├── test_polarCartesianConversion.py │ ├── test_polarConversion.py │ └── util.py ├── requirements-dev.txt ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | .spyproject 92 | 93 | # Rope project settings 94 | .ropeproject 95 | 96 | # mkdocs documentation 97 | /site 98 | 99 | # mypy 100 | .mypy_cache/ 101 | 102 | # PyCharm Project specific 103 | .idea/* 104 | 105 | # Ignore IML file for project since it contains path dependent information 106 | *.iml 107 | 108 | # Ignore dist folder which contains the Python compiled code for creating a standalone executable 109 | dist 110 | 111 | # Ignore vscode settings 112 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 3.6 5 | 6 | # Command to install dependencies 7 | install: 8 | - pip install --upgrade pip 9 | - pip install -r requirements.txt 10 | - pip install -r requirements-dev.txt 11 | 12 | # Command to run tests 13 | script: coverage run -m unittest discover -v polarTransform/tests 14 | 15 | # Command to deploy releases to PyPi 16 | deploy: 17 | provider: pypi 18 | distributions: sdist bdist_wheel 19 | user: addisonElliott 20 | on: 21 | tags: true 22 | password: 23 | secure: u/P4yVsjsiO+pyVCugJ0yaIywRpGd/e+LYtJxxqrprQx+Si4DTdHKpRJ09c/FGoSsAjfyImAw1ErZdRB8HNP4S906DnGBvc/FdZ0/b2+PgzOsiuItJCCBNJZY9kljrPVGCjnvIUsLwkCLDfMeZYIyEE5uGxh6DnVCOiAKRYhm4Q7KJpF0IfVdogvaM0PGsKxrdq58XXnfLPGHXcbhpd8qjXBvnKw0UiDUulBTZOpFD/NSS4VVzcFPHheqUo6x/mrj65AwdL4LRnP96ynKmF703dvPy3QD7KxcluDx2DIUQ6cgnxo4ePgSaWB1Q1biNfALMnoJgYKmNvOvswd+1Z8KbmNsuPuV7P9l0Z8JQnI5H4fsKqx2qymR3lv3IPvUeTA9XSUSQlSM318/NCwjCzPFuWJme6Edt1D1AjIZyrzlegJ3zqJY+awHdK1pgmykKoDoax0EmJH5mguZBGe/PDtHFQk5WNqzF8talpAnhvboLtyuG1MRT36osjy8vaMdrE6/FRzwQp8foJtMeXP2L+fjpK5RYpi7ETj+Hlz2NPv3z6E/KfwfLYnx0EsgGP5lwTeoApcFdYqjy7TB7W9rwyU5irJgT6wbqnjIV3kvElxAc5EJNj4KYkO9L4TDJXbbTGKzrTTFFELtHOi2V2XUze2++utRhfovUTnAnaLATWd89g= 24 | 25 | after_success: 26 | - codecov 27 | 28 | # Only build for master branch and tagged released (must follow semantic versioning syntax) 29 | branches: 30 | only: 31 | - master 32 | - /^v\d+\.\d+(\.\d+)?(-\S*)?$/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Addison Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include polarTransform/tests/data/* -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/addisonElliott/polarTransform.svg?branch=master 2 | :target: https://travis-ci.org/addisonElliott/polarTransform 3 | :alt: Build Status 4 | 5 | .. image:: https://img.shields.io/pypi/pyversions/polarTransform.svg 6 | :target: https://img.shields.io/pypi/pyversions/polarTransform.svg 7 | :alt: Python version 8 | 9 | .. image:: https://badge.fury.io/py/polarTransform.svg 10 | :target: https://badge.fury.io/py/polarTransform 11 | :alt: PyPi version 12 | 13 | .. image:: https://readthedocs.org/projects/polartransform/badge/?version=latest 14 | :target: https://polartransform.readthedocs.io/en/latest/?badge=latest 15 | :alt: Documentation Status 16 | 17 | .. image:: https://codecov.io/gh/addisonElliott/polarTransform/branch/master/graph/badge.svg 18 | :target: https://codecov.io/gh/addisonElliott/polarTransform 19 | 20 | | 21 | 22 | Introduction 23 | ================= 24 | polarTransform is a Python package for converting images between the polar and Cartesian domain. It contains many 25 | features such as specifying the start/stop radius and angle, interpolation order (bicubic, linear, nearest, etc), and 26 | much more. 27 | 28 | Installing 29 | ================= 30 | Prerequisites 31 | ------------- 32 | * Python 3 33 | * Dependencies: 34 | * numpy 35 | * scipy 36 | * scikit-image 37 | 38 | Installing polarTransform 39 | ------------------------- 40 | polarTransform is currently available on `PyPi `_. The simplest way to 41 | install alone is using ``pip`` at a command line:: 42 | 43 | pip install polarTransform 44 | 45 | which installs the latest release. To install the latest code from the repository (usually stable, but may have 46 | undocumented changes or bugs):: 47 | 48 | pip install git+https://github.com/addisonElliott/polarTransform.git 49 | 50 | 51 | For developers, you can clone the polarTransform repository and run the ``setup.py`` file. Use the following commands to get 52 | a copy from GitHub and install all dependencies:: 53 | 54 | git clone pip install git+https://github.com/addisonElliott/polarTransform.git 55 | cd polarTransform 56 | pip install . 57 | 58 | or, for the last line, instead use:: 59 | 60 | pip install -e . 61 | 62 | to install in 'develop' or 'editable' mode, where changes can be made to the local working code and Python will use 63 | the updated polarTransform code. 64 | 65 | Test and coverage 66 | ================= 67 | Run the following command in the base directory to run the tests: 68 | 69 | .. code-block:: bash 70 | 71 | python -m unittest discover -v polarTransform/tests 72 | 73 | Example 74 | ================= 75 | Input image: 76 | 77 | .. image:: http://polartransform.readthedocs.io/en/latest/_images/verticalLines.png 78 | :alt: Cartesian image 79 | 80 | .. code-block:: python 81 | 82 | import polarTransform 83 | import matplotlib.pyplot as plt 84 | import imageio 85 | 86 | verticalLinesImage = imageio.imread('IMAGE_PATH_HERE') 87 | 88 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, initialRadius=30, 89 | finalRadius=100, initialAngle=2 / 4 * np.pi, 90 | finalAngle=5 / 4 * np.pi) 91 | 92 | cartesianImage = ptSettings.convertToCartesianImage(polarImage) 93 | 94 | plt.figure() 95 | plt.imshow(polarImage, origin='lower') 96 | 97 | plt.figure() 98 | plt.imshow(cartesianImage, origin='lower') 99 | 100 | The result is a polar domain image with a specified initial and final radius and angle: 101 | 102 | .. image:: http://polartransform.readthedocs.io/en/latest/_images/verticalLinesPolarImage_scaled3.png 103 | :alt: Polar image 104 | 105 | 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: 106 | 107 | .. image:: http://polartransform.readthedocs.io/en/latest/_images/verticalLinesCartesianImage_scaled.png 108 | :alt: Cartesian image 109 | 110 | Next Steps 111 | ================= 112 | To learn more about polarTransform, see the `documentation `_. 113 | 114 | License 115 | ================= 116 | polarTransform has an MIT-based `license `_. 117 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = polarTransform 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=polarTransform 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/centerAnnotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/centerAnnotations.png -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | .field-list p { 2 | margin: 1em 0; 3 | } -------------------------------------------------------------------------------- /docs/source/_static/shortAxisApex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/shortAxisApex.png -------------------------------------------------------------------------------- /docs/source/_static/shortAxisApexPolarImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/shortAxisApexPolarImage.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLines.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesCartesianImageBorders2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesCartesianImageBorders2.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesCartesianImage_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesCartesianImage_scaled.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesCartesianImage_scaled2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesCartesianImage_scaled2.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesPolarImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesPolarImage.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesPolarImageBorders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesPolarImageBorders.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesPolarImageBorders3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesPolarImageBorders3.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesPolarImage_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesPolarImage_scaled.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesPolarImage_scaled2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesPolarImage_scaled2.png -------------------------------------------------------------------------------- /docs/source/_static/verticalLinesPolarImage_scaled3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/docs/source/_static/verticalLinesPolarImage_scaled3.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # polarTransform documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Mar 2 01:25:32 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('../../')) 23 | import polarTransform 24 | 25 | def setup(app): 26 | app.add_stylesheet('css/custom.css') 27 | 28 | 29 | # -- General configuration ------------------------------------------------ 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ['sphinx.ext.autodoc', 39 | 'sphinx.ext.intersphinx', 40 | 'sphinx.ext.mathjax', 41 | 'sphinx.ext.autosummary', 42 | 'sphinx.ext.ifconfig', 43 | 'sphinx.ext.viewcode', 44 | 'sphinx.ext.githubpages', 45 | 'numpydoc'] 46 | # 'sphinxcontrib.napoleon'] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # Fixes issue with toctree reference to nonexisting document... 61 | numpydoc_show_class_members = False 62 | 63 | # General information about the project. 64 | project = 'polarTransform' 65 | copyright = '2018, Addison Elliott' 66 | author = 'Addison Elliott' 67 | 68 | # The version info for the project you're documenting, acts as replacement for 69 | # |version| and |release|, also used in various other places throughout the 70 | # built documents. 71 | # 72 | # The short X.Y version. 73 | version = polarTransform.__version__ 74 | # The full version, including alpha/beta/rc tags. 75 | release = polarTransform.__version__ 76 | 77 | # The language for content autogenerated by Sphinx. Refer to documentation 78 | # for a list of supported languages. 79 | # 80 | # This is also used if you do content translation via gettext catalogs. 81 | # Usually you set "language" from the command line for these cases. 82 | language = None 83 | 84 | # List of patterns, relative to source directory, that match files and 85 | # directories to ignore when looking for source files. 86 | # This patterns also effect to html_static_path and html_extra_path 87 | exclude_patterns = ['polarTransform.tests.rst'] 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # If true, `todo` and `todoList` produce output, else they produce nothing. 93 | todo_include_todos = False 94 | 95 | 96 | # -- Options for HTML output ---------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | # 101 | html_theme = 'sphinx_rtd_theme' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | # 107 | # html_theme_options = {} 108 | 109 | # Add any paths that contain custom static files (such as style sheets) here, 110 | # relative to this directory. They are copied after the builtin static files, 111 | # so a file named "default.css" will overwrite the builtin "default.css". 112 | html_static_path = ['_static'] 113 | 114 | # Custom sidebar templates, must be a dictionary that maps document names 115 | # to template names. 116 | # 117 | # This is required for the alabaster theme 118 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 119 | html_sidebars = { 120 | '**': [ 121 | 'about.html', 122 | 'navigation.html', 123 | 'relations.html', # needs 'show_related': True theme option to display 124 | 'searchbox.html', 125 | 'donate.html', 126 | ] 127 | } 128 | 129 | 130 | # -- Options for HTMLHelp output ------------------------------------------ 131 | 132 | # Output file base name for HTML help builder. 133 | htmlhelp_basename = 'polarTransformdoc' 134 | 135 | 136 | # -- Options for LaTeX output --------------------------------------------- 137 | 138 | latex_elements = { 139 | # The paper size ('letterpaper' or 'a4paper'). 140 | # 141 | # 'papersize': 'letterpaper', 142 | 143 | # The font size ('10pt', '11pt' or '12pt'). 144 | # 145 | # 'pointsize': '10pt', 146 | 147 | # Additional stuff for the LaTeX preamble. 148 | # 149 | # 'preamble': '', 150 | 151 | # Latex figure (float) alignment 152 | # 153 | # 'figure_align': 'htbp', 154 | } 155 | 156 | # Grouping the document tree into LaTeX files. List of tuples 157 | # (source start file, target name, title, 158 | # author, documentclass [howto, manual, or own class]). 159 | latex_documents = [ 160 | (master_doc, 'polarTransform.tex', 'polarTransform Documentation', 161 | 'Addison Elliott', 'manual'), 162 | ] 163 | 164 | 165 | # -- Options for manual page output --------------------------------------- 166 | 167 | # One entry per manual page. List of tuples 168 | # (source start file, name, description, authors, manual section). 169 | man_pages = [ 170 | (master_doc, 'polarTransform', 'polarTransform Documentation', 171 | [author], 1) 172 | ] 173 | 174 | 175 | # -- Options for Texinfo output ------------------------------------------- 176 | 177 | # Grouping the document tree into Texinfo files. List of tuples 178 | # (source start file, target name, title, author, 179 | # dir menu entry, description, category) 180 | texinfo_documents = [ 181 | (master_doc, 'polarTransform', 'polarTransform Documentation', 182 | author, 'polarTransform', 'One line description of project.', 183 | 'Miscellaneous'), 184 | ] 185 | 186 | # Example configuration for intersphinx: refer to the Python standard library. 187 | intersphinx_mapping = {'https://docs.python.org/': None, 188 | 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 189 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), 190 | 'matplotlib': ('http://matplotlib.sourceforge.net/', None)} -------------------------------------------------------------------------------- /docs/source/getting-started.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | Getting Started 3 | ================ 4 | 5 | Introduction 6 | ============ 7 | polarTransform is a Python package for converting images between the polar and Cartesian domain. It contains many 8 | features such as specifying the start/stop radius and angle, interpolation order (bicubic, linear, nearest, etc), and 9 | much more. 10 | 11 | License 12 | ============ 13 | polarTransform has an MIT-based `license `_. 14 | 15 | Installing 16 | ============ 17 | Prerequisites 18 | ------------- 19 | * Python 3 20 | * Dependencies: 21 | * numpy 22 | * scipy 23 | * scikit-image 24 | 25 | Installing polarTransform 26 | ------------------------- 27 | polarTransform is currently available on `PyPi `_. The simplest way to 28 | install alone is using ``pip`` at a command line:: 29 | 30 | pip install polarTransform 31 | 32 | which installs the latest release. To install the latest code from the repository (usually stable, but may have 33 | undocumented changes or bugs):: 34 | 35 | pip install git+https://github.com/addisonElliott/polarTransform.git 36 | 37 | 38 | For developers, you can clone the pydicom repository and run the ``setup.py`` file. Use the following commands to get 39 | a copy from GitHub and install all dependencies:: 40 | 41 | git clone pip install git+https://github.com/addisonElliott/polarTransform.git 42 | cd polarTransform 43 | pip install . 44 | 45 | or, for the last line, instead use:: 46 | 47 | pip install -e . 48 | 49 | to install in 'develop' or 'editable' mode, where changes can be made to the local working code and Python will use 50 | the updated polarTransform code. 51 | 52 | Test and coverage 53 | ================= 54 | Run the following command in the base directory to run the tests: 55 | 56 | .. code-block:: bash 57 | 58 | python -m unittest discover -v polarTransform/tests 59 | 60 | Using polarTransform 61 | ==================== 62 | Once installed, the package can be imported at a Python command line or used in your own Python program with ``import polarTransform``. See the :doc:`user-guide` for more details of how to use the package. 63 | 64 | Support 65 | =============== 66 | Bugs can be submitted through the `issue tracker `_. 67 | 68 | Pull requests are welcome too! 69 | 70 | Next Steps 71 | =============== 72 | To start learning how to use polarTransform, see the :doc:`user-guide`. 73 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to polarTransform's documentation! 2 | =========================================== 3 | .. toctree:: 4 | :maxdepth: 2 5 | :caption: Contents: 6 | 7 | getting-started 8 | user-guide 9 | polarTransform 10 | 11 | Indices and tables 12 | ================== 13 | * :ref:`genindex` 14 | * :ref:`modindex` 15 | -------------------------------------------------------------------------------- /docs/source/polarTransform.rst: -------------------------------------------------------------------------------- 1 | Reference Guide 2 | =============== 3 | 4 | Table of Contents 5 | ----------------- 6 | 7 | .. autosummary:: 8 | 9 | polarTransform.convertToCartesianImage 10 | polarTransform.convertToPolarImage 11 | polarTransform.getCartesianPointsImage 12 | polarTransform.getPolarPointsImage 13 | polarTransform.ImageTransform 14 | 15 | polarTransform Module 16 | --------------------- 17 | 18 | .. automodule:: polarTransform 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/source/user-guide.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | User Guide 3 | =============== 4 | 5 | .. currentmodule:: polarTransform 6 | 7 | :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. 8 | 9 | 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. 10 | 11 | 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. 12 | 13 | The examples below use images from the test suite. The code snippets should run without modification except for changing the paths to point to the correct image. 14 | 15 | Example 1 16 | -------------- 17 | Let us take a B-mode echocardiogram and convert it to the polar domain. This is essentially reversing the scan conversion done internally by the ultrasound machine. 18 | 19 | Here is the B-mode image: 20 | 21 | .. image:: _static/shortAxisApex.png 22 | :alt: B-mode echocardiogram of short-axis apex view 23 | 24 | .. code-block:: python 25 | 26 | import polarTransform 27 | import matplotlib.pyplot as plt 28 | import imageio 29 | 30 | cartesianImage = imageio.imread('IMAGE_PATH_HERE') 31 | 32 | polarImage, ptSettings = polarTransform.convertToPolarImage(cartesianImage, center=[401, 365]) 33 | plt.imshow(polarImage.T, origin='lower') 34 | 35 | Resulting polar domain image: 36 | 37 | .. image:: _static/shortAxisApexPolarImage.png 38 | :alt: Polar image of echocardiogram of short-axis apex view 39 | 40 | The example input image has a width of 800px and a height of 604px. Since many imaging libraries use C-order rather than Fortran order, the Numpy array containing the image data loaded from imageio has a shape of (604, 800). This is what polarTransform expects for an image where the first dimension is the slowest varying (y) and the last dimension is the fastest varying (x). Additional dimensions can be present before the y & x dimensions in which case polarTransform will transform each 2D slice individually. 41 | 42 | The center argument should be a list, tuple or Numpy array of length 2 with format (x, y). A common theme throughout this library is that points will be specified in Fortran order, i.e. (x, y) or (r, theta) whilst data and image sizes will be specified in C-order, i.e. (y, x) or (theta, r). 43 | 44 | The polar image returned in this example is in C-order. So this means that the data is (theta, r). When displaying an image using matplotlib, the first dimension is y and second is x. The image is transposed before displaying to flip it 90 degrees. 45 | 46 | Example 2 47 | -------------- 48 | Input image: 49 | 50 | .. image:: _static/verticalLines.png 51 | :alt: Cartesian image 52 | 53 | .. code-block:: python 54 | 55 | import polarTransform 56 | import matplotlib.pyplot as plt 57 | import imageio 58 | 59 | verticalLinesImage = imageio.imread('IMAGE_PATH_HERE') 60 | 61 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, initialRadius=30, 62 | finalRadius=100, initialAngle=2 / 4 * np.pi, 63 | finalAngle=5 / 4 * np.pi, hasColor=True) 64 | 65 | cartesianImage = ptSettings.convertToCartesianImage(polarImage) 66 | 67 | plt.figure() 68 | plt.imshow(polarImage.T, origin='lower') 69 | 70 | plt.figure() 71 | plt.imshow(cartesianImage, origin='lower') 72 | 73 | Resulting polar domain image: 74 | 75 | .. image:: _static/verticalLinesPolarImage_scaled3.png 76 | :alt: Polar image 77 | 78 | Converting back to the cartesian image results in: 79 | 80 | .. image:: _static/verticalLinesCartesianImage_scaled.png 81 | :alt: Cartesian image 82 | 83 | Once again, when displaying polar images using matplotlib, the image is first transposed to rotate the image 90 degrees. This makes it easier to view the image because the theta dimension is longer than the radial dimension. 84 | 85 | The hasColor argument was set to True in this example because the image contains color images. The example RGB image has a width and height of 256px. The shape of the image loaded via imageio package is (256, 256, 3). By specified hasColor=True, the last dimension will be shifted to the front and then the polar transformation will occur on each channel separately. Before returning, the function will shift the channel dimension back to the end. If a RGBA image is loaded, it is advised to only transform the first 3 channels and then set the alpha channel to fully on. 86 | -------------------------------------------------------------------------------- /polarTransform/__init__.py: -------------------------------------------------------------------------------- 1 | from polarTransform._version import __version__ 2 | from polarTransform.convertToCartesianImage import convertToCartesianImage 3 | from polarTransform.convertToPolarImage import convertToPolarImage 4 | from polarTransform.imageTransform import ImageTransform 5 | from polarTransform.pointsConversion import getCartesianPointsImage, getPolarPointsImage 6 | 7 | __all__ = ['convertToCartesianImage', 'convertToPolarImage', 'ImageTransform', 'getCartesianPointsImage', 8 | 'getPolarPointsImage' '__version__'] 9 | -------------------------------------------------------------------------------- /polarTransform/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.0.0' 2 | -------------------------------------------------------------------------------- /polarTransform/convertToCartesianImage.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | 3 | import scipy.ndimage 4 | 5 | 6 | def convertToCartesianImage(image, center=None, initialRadius=None, 7 | finalRadius=None, initialAngle=None, 8 | finalAngle=None, imageSize=None, hasColor=False, order=3, border='constant', 9 | borderVal=0.0, useMultiThreading=False, settings=None): 10 | """Convert polar image to cartesian image. 11 | 12 | Using a polar image, this function creates a cartesian image. This function is versatile because it can 13 | automatically calculate an appropriate cartesian image size and center given the polar image. In addition, 14 | parameters for converting to the polar domain are necessary for the conversion back to the cartesian domain. 15 | 16 | Parameters 17 | ---------- 18 | image : N-dimensional :class:`numpy.ndarray` 19 | Polar image to convert to cartesian domain 20 | 21 | Image should be structured in C-order, i.e. the axes should be ordered (..., z, theta, r, [ch]). The channel 22 | axes should only be present if :obj:`hasColor` is :obj:`True`. This format is arbitrary but is selected to stay 23 | consistent with the traditional C-order representation in the Cartesian domain. 24 | 25 | In the mathematical domain, Cartesian coordinates are traditionally represented as (x, y, z) and as 26 | (r, theta, z) in the polar domain. When storing Cartesian data in C-order, the axes are usually flipped and the 27 | data is saved as (z, y, x). Thus, the polar domain coordinates are also flipped to stay consistent, hence the 28 | format (z, theta, r). 29 | 30 | .. note:: 31 | For multi-dimensional images above 2D, the cartesian transformation is applied individually across each 32 | 2D slice. The last two dimensions should be the r & theta dimensions, unless :obj:`hasColor` is True in 33 | which case the 2nd and 3rd to last dimensions should be. The multidimensional shape will be preserved 34 | for the resulting cartesian image (besides the polar dimensions). 35 | center : :class:`str` or (2,) :class:`list`, :class:`tuple` or :class:`numpy.ndarray` of :class:`int`, optional 36 | Specifies the center in the cartesian image to use as the origin in polar domain. The center in the 37 | cartesian domain will be (0, 0) in the polar domain. 38 | 39 | If center is not set, then it will default to ``middle-middle``. If the image size is :obj:`None`, the 40 | center is calculated after the image size is determined. 41 | 42 | For relative positioning within the image, center can be one of the string values in the table below. The 43 | quadrant column contains the visible quadrants for the given center. initialAngle and finalAngle must contain 44 | at least one of the quadrants, otherwise an error will be thrown because the resulting cartesian image is blank. 45 | An example cartesian image is given below with annotations to what the center will be given a center string. 46 | 47 | .. table:: Valid center strings 48 | :widths: auto 49 | 50 | ================ =============== ==================== 51 | Value Quadrant Location in image 52 | ================ =============== ==================== 53 | top-left IV 1 54 | top-middle III, IV 2 55 | top-right III 3 56 | middle-left I, IV 4 57 | middle-middle I, II, III, IV 5 58 | middle-right II, III 6 59 | bottom-left I 7 60 | bottom-middle I, II 8 61 | bottom-right II 9 62 | ================ =============== ==================== 63 | 64 | .. image:: _static/centerAnnotations.png 65 | :alt: Center locations for center strings 66 | initialRadius : :class:`int`, optional 67 | Starting radius in pixels from the center of the cartesian image in the polar image 68 | 69 | The polar image begins at this radius, i.e. the first row of the polar image corresponds to this 70 | starting radius. 71 | 72 | If initialRadius is not set, then it will default to ``0``. 73 | finalRadius : :class:`int`, optional 74 | Final radius in pixels from the center of the cartesian image in the polar image 75 | 76 | The polar image ends at this radius, i.e. the last row of the polar image corresponds to this ending 77 | radius. 78 | 79 | .. note:: 80 | The polar image does **not** include this radius. It includes all radii starting 81 | from initial to final radii **excluding** the final radius. Rather, it will stop one step size before 82 | the final radius. Assuming the radial resolution (see :obj:`radiusSize`) is small enough, this should not 83 | matter. 84 | 85 | If finalRadius is not set, then it will default to the maximum radius which is the size of the radial (1st) 86 | dimension of the polar image. 87 | initialAngle : :class:`float`, optional 88 | Starting angle in radians in the polar image 89 | 90 | The polar image begins at this angle, i.e. the first column of the polar image corresponds to this 91 | starting angle. 92 | 93 | Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range of 94 | 0 to :math:`2\\pi`. 95 | 96 | If initialAngle is not set, then it will default to ``0.0``. 97 | finalAngle : :class:`float`, optional 98 | Final angle in radians in the polar image 99 | 100 | The polar image ends at this angle, i.e. the last column of the polar image corresponds to this 101 | ending angle. 102 | 103 | .. note:: 104 | The polar image does **not** include this angle. It includes all angles starting 105 | from initial to final angle **excluding** the final angle. Rather, it stops one step size before 106 | the final angle. Assuming the angular resolution (see :obj:`angleSize`) is small enough, this should not 107 | matter. 108 | 109 | Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range of 110 | 0 to :math:`2\\pi`. 111 | 112 | If finalAngle is not set, then it will default to :math:`2\\pi`. 113 | imageSize : (2,) :class:`list`, :class:`tuple` or :class:`numpy.ndarray` of :class:`int`, optional 114 | Desired size of cartesian image where 1st dimension is number of rows and 2nd dimension is number of columns 115 | 116 | If imageSize is not set, then it defaults to the size required to fit the entire polar image on a cartesian 117 | image. 118 | hasColor : :class:`bool`, optional 119 | Whether or not the polar image contains color channels 120 | 121 | This means that the image is structured as (..., y, x, ch) or (..., theta, r, ch) for Cartesian or polar 122 | images, respectively. If color channels are present, the last dimension (channel axes) will be shifted to 123 | the front, converted and then shifted back to its original location. 124 | 125 | Default is :obj:`False` 126 | 127 | .. note:: 128 | If an alpha band (4th channel of image is present), then it will be converted. Typically, this is 129 | unwanted, so the recommended solution is to transform the first 3 channels and set the 4th channel to 130 | fully on. 131 | order : :class:`int` (0-5), optional 132 | The order of the spline interpolation, default is 3. The order has to be in the range 0-5. 133 | 134 | The following orders have special names: 135 | 136 | * 0 - nearest neighbor 137 | * 1 - bilinear 138 | * 3 - bicubic 139 | border : {'constant', 'nearest', 'wrap', 'reflect'}, optional 140 | Polar points outside the cartesian image boundaries are filled according to the given mode. 141 | 142 | Default is 'constant' 143 | 144 | The following table describes the mode and expected output when seeking past the boundaries. The input column 145 | is the 1D input array whilst the extended columns on either side of the input array correspond to the expected 146 | values for the given mode if one extends past the boundaries. 147 | 148 | .. table:: Valid border modes and expected output 149 | :widths: auto 150 | 151 | ========== ====== ================= ====== 152 | Mode Ext. Input Ext. 153 | ========== ====== ================= ====== 154 | mirror 4 3 2 1 2 3 4 5 6 7 8 7 6 5 155 | reflect 3 2 1 1 2 3 4 5 6 7 8 8 7 6 156 | nearest 1 1 1 1 2 3 4 5 6 7 8 8 8 8 157 | constant 0 0 0 1 2 3 4 5 6 7 8 0 0 0 158 | wrap 6 7 8 1 2 3 4 5 6 7 8 1 2 3 159 | ========== ====== ================= ====== 160 | 161 | Refer to :func:`scipy.ndimage.map_coordinates` for more details on this argument. 162 | borderVal : same datatype as :obj:`image`, optional 163 | Value used for polar points outside the cartesian image boundaries if :obj:`border` = 'constant'. 164 | 165 | Default is 0.0 166 | useMultiThreading : :class:`bool`, optional 167 | Whether to use multithreading when applying transformation for 3D images. This considerably speeds up the 168 | execution time for large images but adds overhead for smaller 3D images. 169 | 170 | Default is :obj:`False` 171 | settings : :class:`ImageTransform`, optional 172 | Contains metadata for conversion between polar and cartesian image. 173 | 174 | Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and 175 | provides an easy way of passing these parameters along without having to specify them all again. 176 | 177 | .. warning:: 178 | Cleaner and more succint to use :meth:`ImageTransform.convertToCartesianImage` 179 | 180 | If settings is not specified, then the other arguments are used in this function and the defaults will be 181 | calculated if necessary. If settings is given, then the values from settings will be used. 182 | 183 | Returns 184 | ------- 185 | cartesianImage : N-dimensional :class:`numpy.ndarray` 186 | Cartesian image 187 | 188 | Resulting image is structured in C-order, i.e. the axes are ordered as (..., z, y, x, [ch]). This format is 189 | the traditional method of storing image data in Python. 190 | 191 | Resulting image shape will be the same as the input image except for the polar dimensions are 192 | replaced with the Cartesian dimensions. 193 | settings : :class:`ImageTransform` 194 | Contains metadata for conversion between polar and cartesian image. 195 | 196 | Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and 197 | provides an easy way of passing these parameters along without having to specify them all again. 198 | """ 199 | 200 | # If there is a color channel present, move it to the front of axes 201 | if settings.hasColor if settings is not None else hasColor: 202 | image = np.moveaxis(image, -1, 0) 203 | 204 | # Create settings if none are given 205 | if settings is None: 206 | # Center is set to middle-middle, which means all four quadrants will be shown 207 | if center is None: 208 | center = 'middle-middle' 209 | 210 | # Initial radius of the source image 211 | # In other words, what radius does row 0 correspond to? 212 | # If not set, default is 0 to get the entire image 213 | if initialRadius is None: 214 | initialRadius = 0 215 | 216 | # Final radius of the source image 217 | # In other words, what radius does the last row of polar image correspond to? 218 | # If not set, default is the largest radius from image 219 | if finalRadius is None: 220 | finalRadius = image.shape[-1] 221 | 222 | # Initial angle of the source image 223 | # In other words, what angle does column 0 correspond to? 224 | # If not set, default is 0 to get the entire image 225 | if initialAngle is None: 226 | initialAngle = 0 227 | 228 | # Final angle of the source image 229 | # In other words, what angle does the last column of polar image correspond to? 230 | # If not set, default is 2pi to get the entire image 231 | if finalAngle is None: 232 | finalAngle = 2 * np.pi 233 | 234 | # This is used to scale the result of the radius to get the appropriate Cartesian value 235 | scaleRadius = image.shape[-1] / (finalRadius - initialRadius) 236 | 237 | # This is used to scale the result of the angle to get the appropriate Cartesian value 238 | scaleAngle = image.shape[-2] / (finalAngle - initialAngle) 239 | 240 | if imageSize is None: 241 | # Obtain the image size by looping from initial to final source angle (every possible theta in the image 242 | # basically) 243 | thetas = np.mod(np.linspace(0, (finalAngle - initialAngle), image.shape[-2]) + initialAngle, 2 * np.pi) 244 | maxRadius = finalRadius * np.ones_like(thetas) 245 | 246 | # Then get the maximum radius of the image and compute the x/y coordinates for each option 247 | # If a center is not specified, then use the origin as a default. This will be used to determine 248 | # the new center and image size at once 249 | if center is not None and not isinstance(center, str): 250 | xO, yO = getCartesianPoints2(maxRadius, thetas, center) 251 | else: 252 | xO, yO = getCartesianPoints2(maxRadius, thetas, np.array([0, 0])) 253 | 254 | # Finally, get the maximum and minimum x/y to obtain the bounds necessary 255 | # For the minimum x/y, the largest it can be is 0 because of the origin 256 | # For the maximum x/y, the smallest it can be is 0 because of the origin 257 | # This happens when the initial and final source angle are in the same quadrant 258 | # Because of this, it is guaranteed that the min is <= 0 and max is >= 0 259 | xMin, xMax = min(xO.min(), 0), max(xO.max(), 0) 260 | yMin, yMax = min(yO.min(), 0), max(yO.max(), 0) 261 | 262 | # Set the image size and center based on the x/y min/max 263 | if center == 'bottom-left': 264 | imageSize = np.array([yMax, xMax]) 265 | center = np.array([0, 0]) 266 | elif center == 'bottom-middle': 267 | imageSize = np.array([yMax, xMax - xMin]) 268 | center = np.array([xMin, 0]) 269 | elif center == 'bottom-right': 270 | imageSize = np.array([yMax, xMin]) 271 | center = np.array([xMin, 0]) 272 | elif center == 'middle-left': 273 | imageSize = np.array([yMax - yMin, xMax]) 274 | center = np.array([0, yMin]) 275 | elif center == 'middle-middle': 276 | imageSize = np.array([yMax - yMin, xMax - xMin]) 277 | center = np.array([xMin, yMin]) 278 | elif center == 'middle-right': 279 | imageSize = np.array([yMax - yMin, xMin]) 280 | center = np.array([xMin, yMin]) 281 | elif center == 'top-left': 282 | imageSize = np.array([yMin, xMax]) 283 | center = np.array([0, yMin]) 284 | elif center == 'top-middle': 285 | imageSize = np.array([yMin, xMax - xMin]) 286 | center = np.array([xMin, yMin]) 287 | elif center == 'top-right': 288 | imageSize = np.array([yMin, xMin]) 289 | center = np.array([xMin, yMin]) 290 | 291 | # When the image size or center are set to x or y min, then that is a negative value 292 | # Instead of typing abs for each one, an absolute value of the image size and center is done at the end to 293 | # make it easier. 294 | imageSize = np.ceil(np.abs(imageSize)).astype(int) 295 | center = np.ceil(np.abs(center)).astype(int) 296 | elif isinstance(center, str): 297 | # Set the center based on the image size given 298 | if center == 'bottom-left': 299 | center = imageSize[1::-1] * np.array([0, 0]) 300 | elif center == 'bottom-middle': 301 | center = imageSize[1::-1] * np.array([1 / 2, 0]) 302 | elif center == 'bottom-right': 303 | center = imageSize[1::-1] * np.array([1, 0]) 304 | elif center == 'middle-left': 305 | center = imageSize[1::-1] * np.array([0, 1 / 2]) 306 | elif center == 'middle-middle': 307 | center = imageSize[1::-1] * np.array([1 / 2, 1 / 2]) 308 | elif center == 'middle-right': 309 | center = imageSize[1::-1] * np.array([1, 1 / 2]) 310 | elif center == 'top-left': 311 | center = imageSize[1::-1] * np.array([0, 1]) 312 | elif center == 'top-middle': 313 | center = imageSize[1::-1] * np.array([1 / 2, 1]) 314 | elif center == 'top-right': 315 | center = imageSize[1::-1] * np.array([1, 1]) 316 | 317 | # Convert image size to tuple to standardize the variable type 318 | # Some people may use list but we want to convert this 319 | imageSize = tuple(imageSize) 320 | 321 | settings = ImageTransform(center, initialRadius, finalRadius, initialAngle, finalAngle, imageSize, 322 | image.shape[-2:], hasColor) 323 | else: 324 | # This is used to scale the result of the radius to get the appropriate Cartesian value 325 | scaleRadius = settings.polarImageSize[1] / (settings.finalRadius - settings.initialRadius) 326 | 327 | # This is used to scale the result of the angle to get the appropriate Cartesian value 328 | scaleAngle = settings.polarImageSize[0] / (settings.finalAngle - settings.initialAngle) 329 | 330 | # Get list of cartesian x and y coordinate and create a 2D create of the coordinates using meshgrid 331 | xs = np.arange(0, settings.cartesianImageSize[1]) 332 | ys = np.arange(0, settings.cartesianImageSize[0]) 333 | x, y = np.meshgrid(xs, ys) 334 | 335 | # Take cartesian grid and convert to polar coordinates 336 | r, theta = getPolarPoints2(x, y, settings.center) 337 | 338 | # Offset the radius by the initial source radius 339 | r = r - settings.initialRadius 340 | 341 | # Offset the theta angle by the initial source angle 342 | # The theta values may go past 2pi, so they are looped back around by taking modulo with 2pi. 343 | # Note: This assumes initial source angle is positive 344 | theta = np.mod(theta - settings.initialAngle + 2 * np.pi, 2 * np.pi) 345 | 346 | # Scale the radius using scale factor 347 | r = r * scaleRadius 348 | 349 | # Scale the angle from radians to pixels using scale factor 350 | theta = theta * scaleAngle 351 | 352 | # Flatten the desired x/y cartesian points into one 2xN array 353 | desiredCoords = np.vstack((theta.flatten(), r.flatten())) 354 | 355 | # Get the new shape which is the cartesian image shape plus any other dimensions 356 | # Get the new shape of the cartesian image which is the same shape of the polar image except the last two dimensions 357 | # (r & theta) are replaced with the cartesian image size 358 | newShape = image.shape[:-2] + settings.cartesianImageSize 359 | 360 | # Reshape the image to be 3D, flattens the array if > 3D otherwise it makes it 3D with the 3rd dimension a size of 1 361 | image = image.reshape((-1,) + settings.polarImageSize) 362 | 363 | if border == 'constant': 364 | # Pad image by 3 pixels and then offset all of the desired coordinates by 3 365 | image = np.pad(image, ((0, 0), (3, 3), (3, 3)), 'edge') 366 | desiredCoords += 3 367 | 368 | if useMultiThreading: 369 | with concurrent.futures.ThreadPoolExecutor() as executor: 370 | futures = [executor.submit(scipy.ndimage.map_coordinates, slice, desiredCoords, mode=border, cval=borderVal, 371 | order=order) for slice in image] 372 | 373 | concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED) 374 | 375 | cartesianImages = [future.result().reshape(x.shape) for future in futures] 376 | else: 377 | cartesianImages = [] 378 | 379 | # Loop through the third dimension and map each 2D slice 380 | for slice in image: 381 | imageSlice = scipy.ndimage.map_coordinates(slice, desiredCoords, mode=border, cval=borderVal, 382 | order=order).reshape(x.shape) 383 | cartesianImages.append(imageSlice) 384 | 385 | # Stack all of the slices together and reshape it to what it should be 386 | cartesianImage = np.stack(cartesianImages, axis=0).reshape(newShape) 387 | 388 | # If there is a color channel present, move it abck to the end of axes 389 | if settings.hasColor: 390 | cartesianImage = np.moveaxis(cartesianImage, 0, -1) 391 | 392 | return cartesianImage, settings 393 | 394 | 395 | from polarTransform.pointsConversion import * 396 | from polarTransform.imageTransform import ImageTransform 397 | -------------------------------------------------------------------------------- /polarTransform/convertToPolarImage.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | 3 | import scipy.ndimage 4 | 5 | 6 | def convertToPolarImage(image, center=None, initialRadius=None, finalRadius=None, initialAngle=None, finalAngle=None, 7 | radiusSize=None, angleSize=None, hasColor=False, order=3, border='constant', borderVal=0.0, 8 | useMultiThreading=False, settings=None): 9 | """Convert cartesian image to polar image. 10 | 11 | Using a cartesian image, this function creates a polar domain image where the first dimension is radius and 12 | second dimension is the angle. This function is versatile because it allows different starting and stopping 13 | radii and angles to extract the polar region you are interested in. 14 | 15 | .. note:: 16 | Traditionally images are loaded such that the origin is in the upper-left hand corner. In these cases the 17 | :obj:`initialAngle` and :obj:`finalAngle` will rotate clockwise from the x-axis. For simplicitly, it is 18 | recommended to flip the image along first dimension before passing to this function. 19 | 20 | Parameters 21 | ---------- 22 | image : N-dimensional :class:`numpy.ndarray` 23 | Cartesian image to convert to polar domain 24 | 25 | Image should be structured in C-order, i.e. the axes should be ordered as (..., z, y, x, [ch]). This format is 26 | the traditional method of storing image data in Python. 27 | 28 | .. note:: 29 | For multi-dimensional images above 2D, the polar transformation is applied individually across each 2D 30 | slice. The last two dimensions should be the x & y dimensions, unless :obj:`hasColor` is True in which 31 | case the 2nd and 3rd to last dimensions should be. The multidimensional shape will be preserved for the 32 | resulting polar image (besides the Cartesian dimensions). 33 | center : (2,) :class:`list`, :class:`tuple` or :class:`numpy.ndarray` of :class:`int`, optional 34 | Specifies the center in the cartesian image to use as the origin in polar domain. The center in the 35 | cartesian domain will be (0, 0) in the polar domain. 36 | 37 | The center is structured as (x, y) where the first item is the x-coordinate and second item is the y-coordinate. 38 | 39 | If center is not set, then it will default to ``round(image.shape[::-1] / 2)``. 40 | initialRadius : :class:`int`, optional 41 | Starting radius in pixels from the center of the cartesian image that will appear in the polar image 42 | 43 | The polar image will begin at this radius, i.e. the first row of the polar image will correspond to this 44 | starting radius. 45 | 46 | If initialRadius is not set, then it will default to ``0``. 47 | finalRadius : :class:`int`, optional 48 | Final radius in pixels from the center of the cartesian image that will appear in the polar image 49 | 50 | The polar image will end at this radius, i.e. the last row of the polar image will correspond to this ending 51 | radius. 52 | 53 | .. note:: 54 | The polar image will **not** include this radius. It will include all radii starting 55 | from initial to final radii **excluding** the final radius. Rather, it will stop one step size before 56 | the final radius. Assuming the radial resolution (see :obj:`radiusSize`) is small enough, this should not 57 | matter. 58 | 59 | If finalRadius is not set, then it will default to the maximum radius of the cartesian image. Using the 60 | furthest corner from the center, the finalRadius can be calculated as: 61 | 62 | .. math:: 63 | finalRadius = \\sqrt{((X_{max} - X_{center})^2 + (Y_{max} - Y_{center})^2)} 64 | initialAngle : :class:`float`, optional 65 | Starting angle in radians that will appear in the polar image 66 | 67 | The polar image will begin at this angle, i.e. the first column of the polar image will correspond to this 68 | starting angle. 69 | 70 | Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range of 71 | 0 to :math:`2\\pi`. 72 | 73 | If initialAngle is not set, then it will default to ``0.0``. 74 | finalAngle : :class:`float`, optional 75 | Final angle in radians that will appear in the polar image 76 | 77 | The polar image will end at this angle, i.e. the last column of the polar image will correspond to this 78 | ending angle. 79 | 80 | .. note:: 81 | The polar image will **not** include this angle. It will include all angle starting 82 | from initial to final angle **excluding** the final angle. Rather, it will stop one step size before 83 | the final angle. Assuming the angular resolution (see :obj:`angleSize`) is small enough, this should not 84 | matter. 85 | 86 | Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range of 87 | 0 to :math:`2\\pi`. 88 | 89 | If finalAngle is not set, then it will default to :math:`2\\pi`. 90 | radiusSize : :class:`int`, optional 91 | Size of polar image for radial (1st) dimension 92 | 93 | This in effect determines the resolution of the radial dimension of the polar image based on the 94 | :obj:`initialRadius` and :obj:`finalRadius`. Resolution can be calculated using equation below in radial 95 | px per cartesian px: 96 | 97 | .. math:: 98 | radialResolution = \\frac{radiusSize}{finalRadius - initialRadius} 99 | 100 | If radiusSize is not set, then it will default to the minimum size necessary to ensure that image information 101 | is not lost in the transformation. The minimum resolution necessary can be found by finding the smallest 102 | change in radius from two connected pixels in the cartesian image. Through experimentation, there is a 103 | surprisingly close relationship between the maximum difference from width or height of the cartesian image to 104 | the :obj:`center` times two. 105 | 106 | The radiusSize is calculated based on this relationship and is proportional to the :obj:`initialRadius` and 107 | :obj:`finalRadius` given. 108 | angleSize : :class:`int`, optional 109 | Size of polar image for angular (2nd) dimension 110 | 111 | This in effect determines the resolution of the angular dimension of the polar image based on the 112 | :obj:`initialAngle` and :obj:`finalAngle`. Resolution can be calculated using equation below in angular 113 | px per cartesian px: 114 | 115 | .. math:: 116 | angularResolution = \\frac{angleSize}{finalAngle - initialAngle} 117 | 118 | If angleSize is not set, then it will default to the minimum size necessary to ensure that image information 119 | is not lost in the transformation. The minimum resolution necessary can be found by finding the smallest 120 | change in angle from two connected pixels in the cartesian image. 121 | 122 | For a cartesian image with either dimension greater than 500px, the angleSize is set to be **two** times larger 123 | than the largest dimension proportional to :obj:`initialAngle` and :obj:`finalAngle`. Otherwise, for a 124 | cartesian image with both dimensions less than 500px, the angleSize is set to be **four** times larger the 125 | largest dimension proportional to :obj:`initialAngle` and :obj:`finalAngle`. 126 | 127 | .. note:: 128 | The above logic **estimates** the necessary angleSize to reduce image information loss. No algorithm 129 | currently exists for determining the required angleSize. 130 | hasColor : :class:`bool`, optional 131 | Whether or not the cartesian image contains color channels 132 | 133 | This means that the image is structured as (..., y, x, ch) or (..., theta, r, ch) for Cartesian or polar 134 | images, respectively. If color channels are present, the last dimension (channel axes) will be shifted to 135 | the front, converted and then shifted back to its original location. 136 | 137 | Default is :obj:`False` 138 | 139 | .. note:: 140 | If an alpha band (4th channel of image is present), then it will be converted. Typically, this is 141 | unwanted, so the recommended solution is to transform the first 3 channels and set the 4th channel to 142 | fully on. 143 | order : :class:`int` (0-5), optional 144 | The order of the spline interpolation, default is 3. The order has to be in the range 0-5. 145 | 146 | The following orders have special names: 147 | 148 | * 0 - nearest neighbor 149 | * 1 - bilinear 150 | * 3 - bicubic 151 | border : {'constant', 'nearest', 'wrap', 'reflect'}, optional 152 | Polar points outside the cartesian image boundaries are filled according to the given mode. 153 | 154 | Default is 'constant' 155 | 156 | The following table describes the mode and expected output when seeking past the boundaries. The input column 157 | is the 1D input array whilst the extended columns on either side of the input array correspond to the expected 158 | values for the given mode if one extends past the boundaries. 159 | 160 | .. table:: Valid border modes and expected output 161 | :widths: auto 162 | 163 | ========== ====== ================= ====== 164 | Mode Ext. Input Ext. 165 | ========== ====== ================= ====== 166 | mirror 4 3 2 1 2 3 4 5 6 7 8 7 6 5 167 | reflect 3 2 1 1 2 3 4 5 6 7 8 8 7 6 168 | nearest 1 1 1 1 2 3 4 5 6 7 8 8 8 8 169 | constant 0 0 0 1 2 3 4 5 6 7 8 0 0 0 170 | wrap 6 7 8 1 2 3 4 5 6 7 8 1 2 3 171 | ========== ====== ================= ====== 172 | 173 | Refer to :func:`scipy.ndimage.map_coordinates` for more details on this argument. 174 | borderVal : same datatype as :obj:`image`, optional 175 | Value used for polar points outside the cartesian image boundaries if :obj:`border` = 'constant'. 176 | 177 | Default is 0.0 178 | useMultiThreading : :class:`bool`, optional 179 | Whether to use multithreading when applying transformation for 3D images. This considerably speeds up the 180 | execution time for large images but adds overhead for smaller 3D images. 181 | 182 | Default is :obj:`False` 183 | settings : :class:`ImageTransform`, optional 184 | Contains metadata for conversion between polar and cartesian image. 185 | 186 | Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and 187 | provides an easy way of passing these parameters along without having to specify them all again. 188 | 189 | .. warning:: 190 | Cleaner and more succint to use :meth:`ImageTransform.convertToPolarImage` 191 | 192 | If settings is not specified, then the other arguments are used in this function and the defaults will be 193 | calculated if necessary. If settings is given, then the values from settings will be used. 194 | 195 | Returns 196 | ------- 197 | polarImage : N-dimensional :class:`numpy.ndarray` 198 | Polar image 199 | 200 | Resulting image is structured in C-order, i.e. the axes are be ordered as (..., z, theta, r, [ch]) 201 | depending on if the input image was 3D. This format is arbitrary but is selected to stay consistent with 202 | the traditional C-order representation in the Cartesian domain. 203 | 204 | In the mathematical domain, Cartesian 205 | coordinates are traditionally represented as (x, y, z) and as (r, theta, z) in the polar domain. When 206 | storing Cartesian data in C-order, the axes are usually flipped and the data is saved as (z, y, x). Thus, 207 | the polar domain coordinates are also flipped to stay consistent, hence the format (z, theta, r). 208 | 209 | Resulting image shape will be the same as the input image except for the Cartesian dimensions are replaced with 210 | the polar dimensions. 211 | settings : :class:`ImageTransform` 212 | Contains metadata for conversion between polar and cartesian image. 213 | 214 | Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and 215 | provides an easy way of passing these parameters along without having to specify them all again. 216 | """ 217 | 218 | # If there is a color channel present, move it to the front of axes 219 | if settings.hasColor if settings is not None else hasColor: 220 | image = np.moveaxis(image, -1, 0) 221 | 222 | # Create settings if none are given 223 | if settings is None: 224 | # If center is not specified, set to the center of the image 225 | # Image shape is reversed because center is specified as x,y and shape is r,c. 226 | # Fancy indexing says to grab the last element (x) and to the 2nd to last element (y) and reverse them 227 | # Otherwise, make sure the center is a Numpy array 228 | if center is None: 229 | center = (np.array(image.shape[-1:-3:-1]) / 2).astype(int) 230 | else: 231 | center = np.array(center) 232 | 233 | # Initial radius is zero if none is selected 234 | if initialRadius is None: 235 | initialRadius = 0 236 | 237 | # Calculate the maximum radius possible 238 | # Get four corners (indices) of the cartesian image 239 | # Convert the corners to polar and get the largest radius 240 | # This will be the maximum radius to represent the entire image in polar 241 | # For image.shape, grab last 2 elements (y, x). Use -2 in case there is additional dimensions in front 242 | corners = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) * image.shape[-2:] 243 | radii, _ = getPolarPoints2(corners[:, 1], corners[:, 0], center) 244 | maxRadius = np.ceil(radii.max()).astype(int) 245 | 246 | if finalRadius is None: 247 | finalRadius = maxRadius 248 | 249 | # Initial angle of zero if none is selected 250 | if initialAngle is None: 251 | initialAngle = 0 252 | 253 | # Final radius is the size of the image so that all points from cartesian are on the polar image 254 | # Final angle is 2pi to loop throughout entire image 255 | if finalAngle is None: 256 | finalAngle = 2 * np.pi 257 | 258 | # If no radius size is given, then the size will be set to make the radius size twice the size of the largest 259 | # dimension of the image 260 | # There is a surprisingly close relationship between the maximum difference from 261 | # width/height of image to center times two. 262 | # The radius size is proportional to the final radius and initial radius 263 | if radiusSize is None: 264 | cross = np.array([[image.shape[-1] - 1, center[1]], [0, center[1]], [center[0], image.shape[-2] - 1], 265 | [center[0], 0]]) 266 | 267 | radiusSize = np.ceil(np.abs(cross - center).max() * 2 * (finalRadius - initialRadius) / maxRadius) \ 268 | .astype(int) 269 | 270 | # Make the angle size be twice the size of largest dimension for images above 500px, otherwise 271 | # use a factor of 4x. 272 | # This angle size is proportional to the initial and final angle. 273 | # This was experimentally determined to yield the best resolution 274 | # The actual answer for the necessary angle size to represent all of the pixels is 275 | # (finalAngle - initialAngle) / (min(arctan(y / x) - arctan((y - 1) / x))) 276 | # Where the coordinates used in min are the four corners of the cartesian image with the center 277 | # subtracted from it. The minimum will be the corner that is the furthest away from the center 278 | # TODO Find a better solution to determining default angle size (optimum?) 279 | if angleSize is None: 280 | maxSize = np.max(image.shape) 281 | 282 | if maxSize > 500: 283 | angleSize = int(2 * np.max(image.shape) * (finalAngle - initialAngle) / (2 * np.pi)) 284 | else: 285 | angleSize = int(4 * np.max(image.shape) * (finalAngle - initialAngle) / (2 * np.pi)) 286 | 287 | # Create the settings 288 | settings = ImageTransform(center, initialRadius, finalRadius, initialAngle, finalAngle, image.shape[-2:], 289 | (angleSize, radiusSize), hasColor) 290 | 291 | # Create radii from start to finish with radiusSize, do same for theta 292 | # Then create a 2D grid of radius and theta using meshgrid 293 | # Set endpoint to False to NOT include the final sample specified. Think of it like this, if you ask to count from 294 | # 0 to 30, that is 31 numbers not 30. Thus, we count 0...29 to get 30 numbers. 295 | radii = np.linspace(settings.initialRadius, settings.finalRadius, settings.polarImageSize[1], endpoint=False) 296 | theta = np.linspace(settings.initialAngle, settings.finalAngle, settings.polarImageSize[0], endpoint=False) 297 | r, theta = np.meshgrid(radii, theta) 298 | 299 | # Take polar grid and convert to cartesian coordinates 300 | xCartesian, yCartesian = getCartesianPoints2(r, theta, settings.center) 301 | 302 | # Flatten the desired x/y cartesian points into one 2xN array 303 | desiredCoords = np.vstack((yCartesian.flatten(), xCartesian.flatten())) 304 | 305 | # Get the new shape of the polar image which is the same shape of the cartesian image except the last two dimensions 306 | # (x & y) are replaced with the polar image size 307 | newShape = image.shape[:-2] + settings.polarImageSize 308 | 309 | # Reshape the image to be 3D, flattens the array if > 3D otherwise it makes it 3D with the 3rd dimension a size of 1 310 | image = image.reshape((-1,) + settings.cartesianImageSize) 311 | 312 | # If border is set to constant, then pad the image by the edges by 3 pixels. 313 | # If one tries to convert back to cartesian without the borders padded then the border of the cartesian image will 314 | # be corrupted because it will average the pixels with the border value 315 | if border == 'constant': 316 | # Pad image by 3 pixels and then offset all of the desired coordinates by 3 317 | image = np.pad(image, ((0, 0), (3, 3), (3, 3)), 'edge') 318 | desiredCoords += 3 319 | 320 | if useMultiThreading: 321 | with concurrent.futures.ThreadPoolExecutor() as executor: 322 | futures = [executor.submit(scipy.ndimage.map_coordinates, slice, desiredCoords, mode=border, cval=borderVal, 323 | order=order) for slice in image] 324 | 325 | concurrent.futures.wait(futures, return_when=concurrent.futures.ALL_COMPLETED) 326 | 327 | polarImages = [future.result().reshape(r.shape) for future in futures] 328 | else: 329 | polarImages = [] 330 | 331 | # Loop through the third dimension and map each 2D slice 332 | for slice in image: 333 | imageSlice = scipy.ndimage.map_coordinates(slice, desiredCoords, mode=border, cval=borderVal, 334 | order=order).reshape(r.shape) 335 | polarImages.append(imageSlice) 336 | 337 | # Stack all of the slices together and reshape it to what it should be 338 | polarImage = np.stack(polarImages, axis=0).reshape(newShape) 339 | 340 | # If there is a color channel present, move it back to the end of axes 341 | if settings.hasColor: 342 | polarImage = np.moveaxis(polarImage, 0, -1) 343 | 344 | return polarImage, settings 345 | 346 | 347 | from polarTransform.imageTransform import ImageTransform 348 | from polarTransform.pointsConversion import * 349 | -------------------------------------------------------------------------------- /polarTransform/imageTransform.py: -------------------------------------------------------------------------------- 1 | class ImageTransform: 2 | """Class to store settings when converting between cartesian and polar domain""" 3 | 4 | def __init__(self, center, initialRadius, finalRadius, initialAngle, finalAngle, cartesianImageSize, 5 | polarImageSize, hasColor): 6 | """Polar and Cartesian Transform Metadata 7 | 8 | ImageTransform contains polar and cartesian transform metadata for the conversion between the two domains. 9 | This metadata is stored in a class to allow for easy conversion between the domains. 10 | 11 | Parameters 12 | ---------- 13 | center : (2,) :class:`numpy.ndarray` of :class:`int` 14 | Specifies the center in the cartesian image to use as the origin in polar domain. The center in the 15 | cartesian domain will be (0, 0) in the polar domain. 16 | 17 | The center is structured as (x, y) where the first item is the x-coordinate and second item is the 18 | y-coordinate. 19 | initialRadius : :class:`int` 20 | Starting radius in pixels from the center of the cartesian image in the polar image 21 | 22 | The polar image begins at this radius, i.e. the first row of the polar image corresponds to this 23 | starting radius. 24 | finalRadius : :class:`int`, optional 25 | Final radius in pixels from the center of the cartesian image in the polar image 26 | 27 | The polar image ends at this radius, i.e. the last row of the polar image corresponds to this ending 28 | radius. 29 | initialAngle : :class:`float`, optional 30 | Starting angle in radians in the polar image 31 | 32 | The polar image begins at this angle, i.e. the first column of the polar image corresponds to this 33 | starting angle. 34 | 35 | Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range 36 | of 0 to :math:`2\\pi`. 37 | finalAngle : :class:`float`, optional 38 | Final angle in radians in the polar image 39 | 40 | The polar image ends at this angle, i.e. the last column of the polar image corresponds to this 41 | ending angle. 42 | 43 | Radian angle is with respect to the x-axis and rotates counter-clockwise. The angle should be in the range 44 | of 0 to :math:`2\\pi`. 45 | cartesianImageSize : (2,) :class:`tuple` of :class:`int` 46 | Size of cartesian image 47 | polarImageSize : (2,) :class:`tuple` of :class:`int` 48 | Size of polar image 49 | hasColor : :class:`bool`, optional 50 | Whether or not the polar or cartesian image contains color channels 51 | 52 | This means that the image is structured as (..., y, x, ch) or (..., theta, r, ch) for Cartesian or polar 53 | images, respectively. If color channels are present, the last dimension (channel axes) will be shifted to 54 | the front, converted and then shifted back to its original location. 55 | 56 | Default is :obj:`False` 57 | 58 | .. note:: 59 | If an alpha band (4th channel of image is present), then it will be converted. Typically, this is 60 | unwanted, so the recommended solution is to transform the first 3 channels and set the 4th channel to 61 | fully on. 62 | """ 63 | self.center = center 64 | self.initialRadius = initialRadius 65 | self.finalRadius = finalRadius 66 | self.initialAngle = initialAngle 67 | self.finalAngle = finalAngle 68 | self.cartesianImageSize = cartesianImageSize 69 | self.polarImageSize = polarImageSize 70 | self.hasColor = hasColor 71 | 72 | def convertToPolarImage(self, image, order=3, border='constant', borderVal=0.0, useMultiThreading=False): 73 | """Convert cartesian image to polar image. 74 | 75 | Using a cartesian image, this function creates a polar domain image where the first dimension is radius and 76 | second dimension is the angle. This function is versatile because it allows different starting and stopping 77 | radii and angles to extract the polar region you are interested in. 78 | 79 | .. note:: 80 | Traditionally images are loaded such that the origin is in the upper-left hand corner. In these cases the 81 | :obj:`initialAngle` and :obj:`finalAngle` will rotate clockwise from the x-axis. For simplicitly, it is 82 | recommended to flip the image along first dimension before passing to this function. 83 | 84 | Parameters 85 | ---------- 86 | image : N-dimensional :class:`numpy.ndarray` 87 | Cartesian image to convert to polar domain 88 | 89 | Image should be structured in C-order, i.e. the axes should be ordered as (..., z, y, x, [ch]). This format 90 | is the traditional method of storing image data in Python. 91 | 92 | .. note:: 93 | For multi-dimensional images above 2D, the polar transformation is applied individually across each 2D 94 | slice. The last two dimensions should be the x & y dimensions, unless :obj:`hasColor` is True in which 95 | case the 2nd and 3rd to last dimensions should be. The multidimensional shape will be preserved for the 96 | resulting polar image (besides the Cartesian dimensions). 97 | order : :class:`int` (0-5), optional 98 | The order of the spline interpolation, default is 3. The order has to be in the range 0-5. 99 | 100 | The following orders have special names: 101 | 102 | * 0 - nearest neighbor 103 | * 1 - bilinear 104 | * 3 - bicubic 105 | border : {'constant', 'nearest', 'wrap', 'reflect'}, optional 106 | Polar points outside the cartesian image boundaries are filled according to the given mode. 107 | 108 | Default is 'constant' 109 | 110 | The following table describes the mode and expected output when seeking past the boundaries. The input 111 | column is the 1D input array whilst the extended columns on either side of the input array correspond to 112 | the expected values for the given mode if one extends past the boundaries. 113 | 114 | .. table:: Valid border modes and expected output 115 | :widths: auto 116 | 117 | ========== ====== ================= ====== 118 | Mode Ext. Input Ext. 119 | ========== ====== ================= ====== 120 | mirror 4 3 2 1 2 3 4 5 6 7 8 7 6 5 121 | reflect 3 2 1 1 2 3 4 5 6 7 8 8 7 6 122 | nearest 1 1 1 1 2 3 4 5 6 7 8 8 8 8 123 | constant 0 0 0 1 2 3 4 5 6 7 8 0 0 0 124 | wrap 6 7 8 1 2 3 4 5 6 7 8 1 2 3 125 | ========== ====== ================= ====== 126 | 127 | Refer to :func:`scipy.ndimage.map_coordinates` for more details on this argument. 128 | borderVal : same datatype as :obj:`image`, optional 129 | Value used for polar points outside the cartesian image boundaries if :obj:`border` = 'constant'. 130 | 131 | Default is 0.0 132 | 133 | Returns 134 | ------- 135 | polarImage : N-dimensional :class:`numpy.ndarray` 136 | Polar image 137 | 138 | Resulting image is structured in C-order, i.e. the axes are be ordered as (..., z, theta, r, [ch]) 139 | depending on if the input image was 3D. This format is arbitrary but is selected to stay consistent with 140 | the traditional C-order representation in the Cartesian domain. 141 | 142 | In the mathematical domain, Cartesian 143 | coordinates are traditionally represented as (x, y, z) and as (r, theta, z) in the polar domain. When 144 | storing Cartesian data in C-order, the axes are usually flipped and the data is saved as (z, y, x). Thus, 145 | the polar domain coordinates are also flipped to stay consistent, hence the format (z, theta, r). 146 | 147 | Resulting image shape will be the same as the input image except for the Cartesian dimensions are replaced 148 | with the polar dimensions. 149 | """ 150 | image, ptSettings = convertToPolarImage(image, order=order, border=border, borderVal=borderVal, 151 | useMultiThreading=useMultiThreading, settings=self) 152 | return image 153 | 154 | def convertToCartesianImage(self, image, order=3, border='constant', borderVal=0.0, useMultiThreading=False): 155 | """Convert polar image to cartesian image. 156 | 157 | Using a polar image, this function creates a cartesian image. This function is versatile because it can 158 | automatically calculate an appropiate cartesian image size and center given the polar image. In addition, 159 | parameters for converting to the polar domain are necessary for the conversion back to the cartesian domain. 160 | 161 | Parameters 162 | ---------- 163 | image : N-dimensional :class:`numpy.ndarray` 164 | Polar image to convert to cartesian domain 165 | 166 | Image should be structured in C-order, i.e. the axes should be ordered (..., z, theta, r, [ch]). The channel 167 | axes should only be present if :obj:`hasColor` is :obj:`True`. This format is arbitrary but is selected to 168 | stay consistent with the traditional C-order representation in the Cartesian domain. 169 | 170 | In the mathematical domain, Cartesian coordinates are traditionally represented as (x, y, z) and as 171 | (r, theta, z) in the polar domain. When storing Cartesian data in C-order, the axes are usually flipped and 172 | the data is saved as (z, y, x). Thus, the polar domain coordinates are also flipped to stay consistent, 173 | hence the format (z, theta, r). 174 | 175 | .. note:: 176 | For multi-dimensional images above 2D, the cartesian transformation is applied individually across each 177 | 2D slice. The last two dimensions should be the r & theta dimensions, unless :obj:`hasColor` is True in 178 | which case the 2nd and 3rd to last dimensions should be. The multidimensional shape will be preserved 179 | for the resulting cartesian image (besides the polar dimensions). 180 | order : :class:`int` (0-5), optional 181 | The order of the spline interpolation, default is 3. The order has to be in the range 0-5. 182 | 183 | The following orders have special names: 184 | 185 | * 0 - nearest neighbor 186 | * 1 - bilinear 187 | * 3 - bicubic 188 | border : {'constant', 'nearest', 'wrap', 'reflect'}, optional 189 | Polar points outside the cartesian image boundaries are filled according to the given mode. 190 | 191 | Default is 'constant' 192 | 193 | The following table describes the mode and expected output when seeking past the boundaries. The input 194 | column is the 1D input array whilst the extended columns on either side of the input array correspond to 195 | the expected values for the given mode if one extends past the boundaries. 196 | 197 | .. table:: Valid border modes and expected output 198 | :widths: auto 199 | 200 | ========== ====== ================= ====== 201 | Mode Ext. Input Ext. 202 | ========== ====== ================= ====== 203 | mirror 4 3 2 1 2 3 4 5 6 7 8 7 6 5 204 | reflect 3 2 1 1 2 3 4 5 6 7 8 8 7 6 205 | nearest 1 1 1 1 2 3 4 5 6 7 8 8 8 8 206 | constant 0 0 0 1 2 3 4 5 6 7 8 0 0 0 207 | wrap 6 7 8 1 2 3 4 5 6 7 8 1 2 3 208 | ========== ====== ================= ====== 209 | 210 | Refer to :func:`scipy.ndimage.map_coordinates` for more details on this argument. 211 | borderVal : same datatype as :obj:`image`, optional 212 | Value used for polar points outside the cartesian image boundaries if :obj:`border` = 'constant'. 213 | 214 | Default is 0.0 215 | useMultiThreading : :class:`bool`, optional 216 | Whether to use multithreading when applying transformation for 3D images. This considerably speeds up the 217 | execution time for large images but adds overhead for smaller 3D images. 218 | 219 | Default is :obj:`False` 220 | 221 | Returns 222 | ------- 223 | cartesianImage : N-dimensional :class:`numpy.ndarray` 224 | Cartesian image 225 | 226 | Resulting image is structured in C-order, i.e. the axes are ordered as (..., z, y, x, [ch]). This format is 227 | the traditional method of storing image data in Python. 228 | 229 | Resulting image shape will be the same as the input image except for the polar dimensions are 230 | replaced with the Cartesian dimensions. 231 | 232 | See Also 233 | -------- 234 | :meth:`convertToCartesianImage` 235 | """ 236 | image, ptSettings = convertToCartesianImage(image, order=order, border=border, borderVal=borderVal, 237 | useMultiThreading=useMultiThreading, settings=self) 238 | return image 239 | 240 | def getPolarPointsImage(self, points): 241 | """Convert list of cartesian points from image to polar image points based on transform metadata 242 | 243 | .. note:: 244 | This does **not** convert from cartesian to polar points, but rather converts pixels from cartesian image to 245 | pixels from polar image using :class:`ImageTransform`. 246 | 247 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 248 | 249 | Parameters 250 | ---------- 251 | points : (N, 2) or (2,) :class:`numpy.ndarray` 252 | List of cartesian points to convert to polar domain 253 | 254 | First column is x and second column is y 255 | 256 | Returns 257 | ------- 258 | polarPoints : (N, 2) or (2,) :class:`numpy.ndarray` 259 | Corresponding polar points from cartesian :obj:`points` using :class:`ImageTransform` 260 | 261 | See Also 262 | -------- 263 | :meth:`getPolarPointsImage`, :meth:`getPolarPoints`, :meth:`getPolarPoints2` 264 | """ 265 | 266 | return getPolarPointsImage(points, self) 267 | 268 | def getCartesianPointsImage(self, points): 269 | """Convert list of polar points from image to cartesian image points based on transform metadata 270 | 271 | .. note:: 272 | This does **not** convert from polar to cartesian points, but rather converts pixels from polar image to 273 | pixels from cartesian image using :class:`ImageTransform`. 274 | 275 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 276 | 277 | Parameters 278 | ---------- 279 | points : (N, 2) or (2,) :class:`numpy.ndarray` 280 | List of polar points to convert to cartesian domain 281 | 282 | First column is r and second column is theta 283 | 284 | Returns 285 | ------- 286 | cartesianPoints : (N, 2) or (2,) :class:`numpy.ndarray` 287 | Corresponding cartesian points from polar :obj:`points` using :class:`ImageTransform` 288 | 289 | See Also 290 | -------- 291 | :meth:`getCartesianPointsImage`, :meth:`getCartesianPoints`, :meth:`getCartesianPoints2` 292 | """ 293 | return getCartesianPointsImage(points, self) 294 | 295 | def __repr__(self): 296 | return 'ImageTransform(center=%s, initialRadius=%i, finalRadius=%i, initialAngle=%f, finalAngle=%f, ' \ 297 | 'cartesianImageSize=%s, polarImageSize=%s)' % (self.center, self.initialRadius, self.finalRadius, 298 | self.initialAngle, self.finalAngle, 299 | self.cartesianImageSize, self.polarImageSize) 300 | 301 | def __str__(self): 302 | return self.__repr__() 303 | 304 | # Bypasses issue with ImageTransform not being defined for cyclic imports 305 | # The answer is to include imports at the end so that everything is already defined before you import anything else 306 | from polarTransform.convertToCartesianImage import convertToCartesianImage 307 | from polarTransform.convertToPolarImage import convertToPolarImage 308 | from polarTransform.pointsConversion import getCartesianPointsImage, getPolarPointsImage 309 | -------------------------------------------------------------------------------- /polarTransform/pointsConversion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def getCartesianPoints(rTheta, center): 5 | """Convert list of polar points to cartesian points 6 | 7 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 8 | 9 | Parameters 10 | ---------- 11 | rTheta : (N, 2) or (2,) :class:`numpy.ndarray` 12 | List of cartesian points to convert to polar domain 13 | 14 | First column is r and second column is theta 15 | center : (2,) :class:`numpy.ndarray` 16 | Center to use for conversion to cartesian domain of polar points 17 | 18 | Format of center is (x, y) 19 | 20 | Returns 21 | ------- 22 | cartesianPoints : (N, 2) :class:`numpy.ndarray` 23 | Corresponding cartesian points from cartesian :obj:`rTheta` 24 | 25 | First column is x and second column is y 26 | 27 | See Also 28 | -------- 29 | :meth:`getCartesianPoints2` 30 | """ 31 | if rTheta.ndim == 2: 32 | x = rTheta[:, 0] * np.cos(rTheta[:, 1]) + center[0] 33 | y = rTheta[:, 0] * np.sin(rTheta[:, 1]) + center[1] 34 | else: 35 | x = rTheta[0] * np.cos(rTheta[1]) + center[0] 36 | y = rTheta[0] * np.sin(rTheta[1]) + center[1] 37 | 38 | return np.array([x, y]).T 39 | 40 | 41 | def getCartesianPoints2(r, theta, center): 42 | """Convert list of polar points to cartesian points 43 | 44 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 45 | 46 | Parameters 47 | ---------- 48 | r : (N,) :class:`numpy.ndarray` 49 | List of polar r points to convert to cartesian domain 50 | theta : (N,) :class:`numpy.ndarray` 51 | List of polar theta points to convert to cartesian domain 52 | center : (2,) :class:`numpy.ndarray` 53 | Center to use for conversion to cartesian domain of polar points 54 | 55 | Format of center is (x, y) 56 | 57 | Returns 58 | ------- 59 | x : (N,) :class:`numpy.ndarray` 60 | Corresponding x points from polar :obj:`r` and :obj:`theta` 61 | y : (N,) :class:`numpy.ndarray` 62 | Corresponding y points from polar :obj:`r` and :obj:`theta` 63 | 64 | See Also 65 | -------- 66 | :meth:`getCartesianPoints` 67 | """ 68 | x = r * np.cos(theta) + center[0] 69 | y = r * np.sin(theta) + center[1] 70 | 71 | return x, y 72 | 73 | 74 | def getPolarPoints(xy, center): 75 | """Convert list of cartesian points to polar points 76 | 77 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 78 | 79 | Parameters 80 | ---------- 81 | xy : (N, 2) or (2,) :class:`numpy.ndarray` 82 | List of cartesian points to convert to polar domain 83 | 84 | First column is x and second column is y 85 | center : (2,) :class:`numpy.ndarray` 86 | Center to use for conversion to polar domain of cartesian points 87 | 88 | Format of center is (x, y) 89 | 90 | Returns 91 | ------- 92 | polarPoints : (N, 2) :class:`numpy.ndarray` 93 | Corresponding polar points from cartesian :obj:`xy` 94 | 95 | First column is r and second column is theta 96 | 97 | See Also 98 | -------- 99 | :meth:`getPolarPoints2` 100 | """ 101 | if xy.ndim == 2: 102 | cX, cY = xy[:, 0] - center[0], xy[:, 1] - center[1] 103 | else: 104 | cX, cY = xy[0] - center[0], xy[1] - center[1] 105 | 106 | r = np.sqrt(cX ** 2 + cY ** 2) 107 | theta = np.arctan2(cY, cX) 108 | 109 | # Make range of theta 0 -> 2pi instead of -pi -> pi 110 | # According to StackOverflow, this is the fastest method: 111 | # https://stackoverflow.com/questions/37358016/numpy-converting-range-of-angles-from-pi-pi-to-0-2pi 112 | theta = np.where(theta < 0, theta + 2 * np.pi, theta) 113 | 114 | return np.array([r, theta]).T 115 | 116 | 117 | def getPolarPoints2(x, y, center): 118 | """Convert list of cartesian points to polar points 119 | 120 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 121 | 122 | Parameters 123 | ---------- 124 | x : (N,) :class:`numpy.ndarray` 125 | List of cartesian x points to convert to polar domain 126 | y : (N,) :class:`numpy.ndarray` 127 | List of cartesian y points to convert to polar domain 128 | center : (2,) :class:`numpy.ndarray` 129 | Center to use for conversion to polar domain of cartesian points 130 | 131 | Format of center is (x, y) 132 | 133 | Returns 134 | ------- 135 | r : (N,) :class:`numpy.ndarray` 136 | Corresponding radii points from cartesian :obj:`x` and :obj:`y` 137 | theta : (N,) :class:`numpy.ndarray` 138 | Corresponding theta points from cartesian :obj:`x` and :obj:`y` 139 | 140 | See Also 141 | -------- 142 | :meth:`getPolarPoints` 143 | """ 144 | cX, cY = x - center[0], y - center[1] 145 | 146 | r = np.sqrt(cX ** 2 + cY ** 2) 147 | 148 | theta = np.arctan2(cY, cX) 149 | 150 | # Make range of theta 0 -> 2pi instead of -pi -> pi 151 | # According to StackOverflow, this is the fastest method: 152 | # https://stackoverflow.com/questions/37358016/numpy-converting-range-of-angles-from-pi-pi-to-0-2pi 153 | theta = np.where(theta < 0, theta + 2 * np.pi, theta) 154 | 155 | return r, theta 156 | 157 | 158 | def getPolarPointsImage(points, settings): 159 | """Convert list of cartesian points from image to polar image points based on transform metadata 160 | 161 | .. warning:: 162 | Cleaner and more succinct to use :meth:`ImageTransform.getPolarPointsImage` 163 | 164 | .. note:: 165 | This does **not** convert from cartesian to polar points, but rather converts pixels from cartesian image to 166 | pixels from polar image using :class:`ImageTransform`. 167 | 168 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 169 | 170 | Parameters 171 | ---------- 172 | points : (N, 2) or (2,) :class:`numpy.ndarray` 173 | List of cartesian points to convert to polar domain 174 | 175 | First column is x and second column is y 176 | settings : :class:`ImageTransform` 177 | Contains metadata for conversion from polar to cartesian domain 178 | 179 | Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and 180 | provides an easy way of passing these parameters along without having to specify them all again. 181 | 182 | Returns 183 | ------- 184 | polarPoints : (N, 2) or (2,) :class:`numpy.ndarray` 185 | Corresponding polar points from cartesian :obj:`points` using :obj:`settings` 186 | 187 | See Also 188 | -------- 189 | :meth:`ImageTransform.getPolarPointsImage`, :meth:`getPolarPoints`, :meth:`getPolarPoints2` 190 | """ 191 | # Convert points to NumPy array 192 | points = np.asanyarray(points) 193 | 194 | # If there is only one point specified and number of dimensions is only one, then make the array a 1x2 array so that 195 | # points[:, 0/1] will not throw an error 196 | if points.ndim == 1 and points.shape[0] == 2: 197 | points = np.expand_dims(points, axis=0) 198 | needSqueeze = True 199 | else: 200 | needSqueeze = False 201 | 202 | # This is used to scale the result of the radius to get the appropriate Cartesian value 203 | scaleRadius = settings.polarImageSize[1] / (settings.finalRadius - settings.initialRadius) 204 | 205 | # This is used to scale the result of the angle to get the appropriate Cartesian value 206 | scaleAngle = settings.polarImageSize[0] / (settings.finalAngle - settings.initialAngle) 207 | 208 | # Take cartesian grid and convert to polar coordinates 209 | polarPoints = getPolarPoints(points, settings.center) 210 | 211 | # Offset the radius by the initial source radius 212 | polarPoints[:, 0] = polarPoints[:, 0] - settings.initialRadius 213 | 214 | # Offset the theta angle by the initial source angle 215 | # The theta values may go past 2pi, so they are looped back around by taking modulo with 2pi. 216 | # Note: This assumes initial source angle is positive 217 | # theta = np.mod(theta - initialAngle + 2 * np.pi, 2 * np.pi) 218 | polarPoints[:, 1] = np.mod(polarPoints[:, 1] - settings.initialAngle + 2 * np.pi, 2 * np.pi) 219 | 220 | # Scale the radius using scale factor 221 | # Scale the angle from radians to pixels using scale factor 222 | polarPoints = polarPoints * [scaleRadius, scaleAngle] 223 | 224 | if needSqueeze: 225 | return np.squeeze(polarPoints) 226 | else: 227 | return polarPoints 228 | 229 | 230 | def getCartesianPointsImage(points, settings): 231 | """Convert list of polar points from image to cartesian image points based on transform metadata 232 | 233 | .. warning:: 234 | Cleaner and more succinct to use :meth:`ImageTransform.getCartesianPointsImage` 235 | 236 | .. note:: 237 | This does **not** convert from polar to cartesian points, but rather converts pixels from polar image to 238 | pixels from cartesian image using :class:`ImageTransform`. 239 | 240 | The returned points are not rounded to the nearest point. User must do that by hand if desired. 241 | 242 | Parameters 243 | ---------- 244 | points : (N, 2) or (2,) :class:`numpy.ndarray` 245 | List of polar points to convert to cartesian domain 246 | 247 | First column is r and second column is theta 248 | settings : :class:`ImageTransform` 249 | Contains metadata for conversion from polar to cartesian domain 250 | 251 | Settings contains many of the arguments in :func:`convertToPolarImage` and :func:`convertToCartesianImage` and 252 | provides an easy way of passing these parameters along without having to specify them all again. 253 | 254 | Returns 255 | ------- 256 | cartesianPoints : (N, 2) or (2,) :class:`numpy.ndarray` 257 | Corresponding cartesian points from polar :obj:`points` using :obj:`settings` 258 | 259 | See Also 260 | -------- 261 | :meth:`ImageTransform.getCartesianPointsImage`, :meth:`getCartesianPoints`, :meth:`getCartesianPoints2` 262 | """ 263 | # Convert points to NumPy array 264 | points = np.asanyarray(points) 265 | 266 | # If there is only one point specified and number of dimensions is only one, then make the array a 1x2 array so that 267 | # points[:, 0/1] will not throw an error 268 | if points.ndim == 1 and points.shape[0] == 2: 269 | points = np.expand_dims(points, axis=0) 270 | needSqueeze = True 271 | else: 272 | needSqueeze = False 273 | 274 | # This is used to scale the result of the radius to get the appropriate Cartesian value 275 | scaleRadius = settings.polarImageSize[1] / (settings.finalRadius - settings.initialRadius) 276 | 277 | # This is used to scale the result of the angle to get the appropriate Cartesian value 278 | scaleAngle = settings.polarImageSize[0] / (settings.finalAngle - settings.initialAngle) 279 | 280 | # Create a new copy of the points variable because we are going to change it and don't want the points parameter to 281 | # change outside of this function 282 | points = points.copy() 283 | 284 | # Scale the radius using scale factor 285 | # Scale the angle from radians to pixels using scale factor 286 | points = points / [scaleRadius, scaleAngle] 287 | 288 | # Offset the radius by the initial source radius 289 | points[:, 0] = points[:, 0] + settings.initialRadius 290 | 291 | # Offset the theta angle by the initial source angle 292 | # The theta values may go past 2pi, so they are looped back around by taking modulo with 2pi. 293 | # Note: This assumes initial source angle is positive 294 | # theta = np.mod(theta - initialAngle + 2 * np.pi, 2 * np.pi) 295 | points[:, 1] = np.mod(points[:, 1] + settings.initialAngle + 2 * np.pi, 2 * np.pi) 296 | 297 | # Take cartesian grid and convert to polar coordinates 298 | cartesianPoints = getCartesianPoints(points, settings.center) 299 | 300 | if needSqueeze: 301 | return np.squeeze(cartesianPoints) 302 | else: 303 | return cartesianPoints 304 | -------------------------------------------------------------------------------- /polarTransform/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | # Required for find_packages to retrieve the test Python files -------------------------------------------------------------------------------- /polarTransform/tests/data/horizontalLines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/horizontalLines.png -------------------------------------------------------------------------------- /polarTransform/tests/data/horizontalLinesAnimated.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/horizontalLinesAnimated.avi -------------------------------------------------------------------------------- /polarTransform/tests/data/horizontalLinesAnimatedPolar.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/horizontalLinesAnimatedPolar.avi -------------------------------------------------------------------------------- /polarTransform/tests/data/horizontalLinesPolarImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/horizontalLinesPolarImage.png -------------------------------------------------------------------------------- /polarTransform/tests/data/shortAxisApex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/shortAxisApex.png -------------------------------------------------------------------------------- /polarTransform/tests/data/shortAxisApexPolarImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/shortAxisApexPolarImage.png -------------------------------------------------------------------------------- /polarTransform/tests/data/shortAxisApexPolarImage_centerMiddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/shortAxisApexPolarImage_centerMiddle.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLines.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesAnimated.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesAnimated.avi -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesAnimatedPolar.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesAnimatedPolar.avi -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesCartesianImageBorders2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesCartesianImageBorders2.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesCartesianImageBorders4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesCartesianImageBorders4.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesCartesianImage_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesCartesianImage_scaled.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesCartesianImage_scaled2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesCartesianImage_scaled2.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesCartesianImage_scaled3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesCartesianImage_scaled3.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesPolarImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesPolarImage.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesPolarImageBorders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesPolarImageBorders.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesPolarImageBorders3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesPolarImageBorders3.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesPolarImage_scaled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesPolarImage_scaled.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesPolarImage_scaled2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesPolarImage_scaled2.png -------------------------------------------------------------------------------- /polarTransform/tests/data/verticalLinesPolarImage_scaled3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/polarTransform/47d814daf14403d2f27da8c3b49f5ad4e01edb7d/polarTransform/tests/data/verticalLinesPolarImage_scaled3.png -------------------------------------------------------------------------------- /polarTransform/tests/generateTests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # Required specifically in each module so that searches happen at the parent directory for importing modules 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 6 | 7 | import polarTransform 8 | from polarTransform.tests.util import * 9 | 10 | # This file should only be ran to generate images contained in the data folder. Since much of this library is performing 11 | # actions on images, the best way to validate through tests that it is working correctly, is to generate images using 12 | # the library and then visually inspecting that the image looks correct. 13 | # 14 | # These images are uploaded and are apart of the repository itself so most of these images will not need to be 15 | # regenerated unless a breaking change is made to the code that changes the output. 16 | 17 | # Load input images that are used to generate output images 18 | shortAxisApexImage = loadImage('shortAxisApex.png') 19 | verticalLinesImage = loadImage('verticalLines.png') 20 | horizontalLines = loadImage('horizontalLines.png', convertToGrayscale=True) 21 | 22 | shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') 23 | shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') 24 | verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') 25 | verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') 26 | verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') 27 | verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') 28 | 29 | verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') 30 | 31 | verticalLinesAnimated = loadVideo('verticalLinesAnimated.avi') 32 | horizontalLinesAnimated = loadVideo('horizontalLinesAnimated.avi', convertToGrayscale=True) 33 | 34 | 35 | # Generate functions 36 | def generateShortAxisPolar(): 37 | polarImage, ptSettings = polarTransform.convertToPolarImage(shortAxisApexImage, center=[401, 365]) 38 | saveImage('shortAxisApexPolarImage.png', polarImage) 39 | 40 | 41 | def generateShortAxisPolar2(): 42 | polarImage, ptSettings = polarTransform.convertToPolarImage(shortAxisApexImage) 43 | saveImage('shortAxisApexPolarImage_centerMiddle.png', polarImage) 44 | 45 | 46 | def generateVerticalLinesPolar(): 47 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, hasColor=True) 48 | saveImage('verticalLinesPolarImage.png', polarImage) 49 | 50 | 51 | def generateVerticalLinesPolar2(): 52 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, initialRadius=30, finalRadius=100, 53 | initialAngle=2 / 4 * np.pi, finalAngle=5 / 4 * np.pi, 54 | radiusSize=140, angleSize=700, hasColor=True) 55 | saveImage('verticalLinesPolarImage_scaled.png', polarImage) 56 | 57 | 58 | def generateVerticalLinesPolar3(): 59 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, initialRadius=30, 60 | finalRadius=100, hasColor=True) 61 | saveImage('verticalLinesPolarImage_scaled2.png', polarImage) 62 | 63 | 64 | def generateVerticalLinesPolar4(): 65 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, initialRadius=30, 66 | finalRadius=100, initialAngle=2 / 4 * np.pi, 67 | finalAngle=5 / 4 * np.pi, hasColor=True) 68 | saveImage('verticalLinesPolarImage_scaled3.png', polarImage) 69 | 70 | 71 | def generateVerticalLinesCartesian2(): 72 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage_scaled, 73 | initialRadius=30, finalRadius=100, 74 | initialAngle=2 / 4 * np.pi, 75 | finalAngle=5 / 4 * np.pi, imageSize=[256, 256], 76 | center=[128, 128], hasColor=True) 77 | saveImage('verticalLinesCartesianImage_scaled.png', cartesianImage) 78 | 79 | 80 | def generateVerticalLinesCartesian3(): 81 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage_scaled2, 82 | center=[128, 128], imageSize=[256, 256], 83 | initialRadius=30, finalRadius=100, 84 | hasColor=True) 85 | saveImage('verticalLinesCartesianImage_scaled2.png', cartesianImage) 86 | 87 | 88 | def generateVerticalLinesCartesian4(): 89 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(verticalLinesPolarImage_scaled3, 90 | initialRadius=30, finalRadius=100, 91 | initialAngle=2 / 4 * np.pi, 92 | finalAngle=5 / 4 * np.pi, center=[128, 128], 93 | imageSize=[256, 256], hasColor=True) 94 | saveImage('verticalLinesCartesianImage_scaled3.png', cartesianImage) 95 | 96 | 97 | def generateVerticalLinesBorders(): 98 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, border='constant', borderVal=128.0, 99 | hasColor=True) 100 | saveImage('verticalLinesPolarImageBorders.png', polarImage) 101 | 102 | ptSettings.cartesianImageSize = (500, 500) 103 | ptSettings.center = np.array([250, 250]) 104 | cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='constant', borderVal=255.0) 105 | saveImage('verticalLinesCartesianImageBorders2.png', cartesianImage) 106 | 107 | 108 | def generateVerticalLinesBorders2(): 109 | polarImage, ptSettings = polarTransform.convertToPolarImage(verticalLinesImage, hasColor=True, border='nearest') 110 | saveImage('verticalLinesPolarImageBorders3.png', polarImage) 111 | 112 | ptSettings.cartesianImageSize = (500, 500) 113 | ptSettings.center = np.array([250, 250]) 114 | cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='nearest') 115 | saveImage('verticalLinesCartesianImageBorders4.png', cartesianImage) 116 | 117 | 118 | def generateHorizontalLinesPolar(): 119 | polarImage, ptSettings = polarTransform.convertToPolarImage(horizontalLines) 120 | saveImage('horizontalLinesPolarImage.png', polarImage) 121 | 122 | 123 | def generateVerticalLinesAnimated(): 124 | frameSize = 40 125 | 126 | frames = [np.roll(verticalLinesImage, 12 * x, axis=1) for x in range(frameSize)] 127 | image3D = np.stack(frames, axis=0) 128 | 129 | saveVideo('verticalLinesAnimated.avi', image3D) 130 | 131 | 132 | def generateVerticalLinesAnimatedPolar(): 133 | frameSize = 40 134 | ptSettings = None 135 | polarFrames = [] 136 | 137 | for frame in verticalLinesAnimated: 138 | # Call convert to polar image on each frame, uses the assumption that individual 2D image works fine based on 139 | # other tests 140 | if ptSettings: 141 | polarFrame = ptSettings.convertToPolarImage(frame) 142 | else: 143 | polarFrame, ptSettings = polarTransform.convertToPolarImage(frame, hasColor=True) 144 | 145 | polarFrames.append(polarFrame) 146 | 147 | polarImage3D = np.stack(polarFrames, axis=0) 148 | saveVideo('verticalLinesAnimatedPolar.avi', polarImage3D) 149 | 150 | 151 | def generateHorizontalLinesAnimated(): 152 | frameSize = 40 153 | 154 | frames = [np.roll(horizontalLines, 36 * x, axis=0) for x in range(frameSize)] 155 | image3D = np.stack(frames, axis=0) 156 | 157 | saveVideo('horizontalLinesAnimated.avi', image3D) 158 | 159 | 160 | def generateHorizontalLinesAnimatedPolar(): 161 | frameSize = 40 162 | ptSettings = None 163 | polarFrames = [] 164 | 165 | for frame in horizontalLinesAnimated: 166 | # Call convert to polar image on each frame, uses the assumption that individual 2D image works fine based on 167 | # other tests 168 | if ptSettings: 169 | polarFrame = ptSettings.convertToPolarImage(frame) 170 | else: 171 | polarFrame, ptSettings = polarTransform.convertToPolarImage(frame) 172 | 173 | polarFrames.append(polarFrame) 174 | 175 | polarImage3D = np.stack(polarFrames, axis=0) 176 | saveVideo('horizontalLinesAnimatedPolar.avi', polarImage3D) 177 | 178 | # Enable these functions as you see fit to generate the images 179 | # Note: It is up to the developer to visually inspect the output images that are created. 180 | # generateShortAxisPolar() 181 | # generateShortAxisPolar2() 182 | # generateVerticalLinesPolar() 183 | # generateVerticalLinesPolar2() 184 | # generateVerticalLinesPolar3() 185 | # generateVerticalLinesPolar4() 186 | # 187 | # generateVerticalLinesCartesian2() 188 | # generateVerticalLinesCartesian3() 189 | # generateVerticalLinesCartesian4() 190 | # 191 | # generateVerticalLinesBorders() 192 | # generateVerticalLinesBorders2() 193 | # 194 | # generateHorizontalLinesPolar() 195 | # 196 | # generateVerticalLinesAnimated() 197 | # generateVerticalLinesAnimatedPolar() 198 | # 199 | # generateHorizontalLinesAnimated() 200 | # generateHorizontalLinesAnimatedPolar() 201 | -------------------------------------------------------------------------------- /polarTransform/tests/test_cartesianConversion.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | # Required specifically in each module so that searches happen at the parent directory for importing modules 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 7 | 8 | import polarTransform 9 | from polarTransform.tests.util import * 10 | 11 | 12 | class TestCartesianConversion(unittest.TestCase): 13 | def setUp(self): 14 | self.shortAxisApexImage = loadImage('shortAxisApex.png') 15 | self.verticalLinesImage = loadImage('verticalLines.png') 16 | self.horizontalLinesImage = loadImage('horizontalLines.png', convertToGrayscale=True) 17 | 18 | self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') 19 | self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') 20 | self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') 21 | self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') 22 | self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') 23 | self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') 24 | 25 | self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') 26 | self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') 27 | self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') 28 | 29 | self.horizontalLinesPolarImage = loadImage('horizontalLinesPolarImage.png', convertToGrayscale=True) 30 | 31 | self.verticalLinesAnimated = loadVideo('verticalLinesAnimated.avi') 32 | self.verticalLinesAnimatedPolar = loadVideo('verticalLinesAnimatedPolar.avi') 33 | self.horizontalLinesAnimated = loadVideo('horizontalLinesAnimated.avi', convertToGrayscale=True) 34 | self.horizontalLinesAnimatedPolar = loadVideo('horizontalLinesAnimatedPolar.avi', convertToGrayscale=True) 35 | 36 | def test_defaultCenter(self): 37 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage, 38 | center=(401, 365), imageSize=(608, 800), 39 | finalRadius=543) 40 | 41 | np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) 42 | self.assertEqual(ptSettings.initialRadius, 0) 43 | self.assertEqual(ptSettings.finalRadius, 543) 44 | self.assertEqual(ptSettings.initialAngle, 0.0) 45 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 46 | self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) 47 | self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[-2:]) 48 | 49 | assert_image_approx_equal_average(cartesianImage, self.shortAxisApexImage, 5) 50 | 51 | def test_notNumpyArrayCenter(self): 52 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage_centerMiddle, 53 | imageSize=(608, 800), finalRadius=503) 54 | 55 | np.testing.assert_array_equal(ptSettings.center, np.array([400, 304])) 56 | self.assertEqual(ptSettings.initialRadius, 0) 57 | self.assertEqual(ptSettings.finalRadius, 503) 58 | self.assertEqual(ptSettings.initialAngle, 0.0) 59 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 60 | self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) 61 | self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage_centerMiddle.shape[-2:]) 62 | 63 | assert_image_approx_equal_average(cartesianImage, self.shortAxisApexImage, 5) 64 | 65 | def test_RGBA(self): 66 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage, 67 | center=(128, 128), imageSize=(256, 256), 68 | finalRadius=182, hasColor=True) 69 | 70 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 71 | self.assertEqual(ptSettings.initialRadius, 0) 72 | self.assertEqual(ptSettings.finalRadius, 182) 73 | self.assertEqual(ptSettings.initialAngle, 0.0) 74 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 75 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 76 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage.shape[-3:-1]) 77 | 78 | assert_image_approx_equal_average(cartesianImage, self.verticalLinesImage, 10) 79 | 80 | def test_IFRadius(self): 81 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, 82 | center=(128, 128), imageSize=(256, 256), 83 | initialRadius=30, finalRadius=100, 84 | hasColor=True) 85 | 86 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 87 | self.assertEqual(ptSettings.initialRadius, 30) 88 | self.assertEqual(ptSettings.finalRadius, 100) 89 | self.assertEqual(ptSettings.initialAngle, 0.0) 90 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 91 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 92 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled2.shape[-3:-1]) 93 | 94 | np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled2) 95 | 96 | def test_IFRadiusAngle(self): 97 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled3, 98 | initialRadius=30, 99 | finalRadius=100, initialAngle=2 / 4 * np.pi, 100 | finalAngle=5 / 4 * np.pi, center=(128, 128), 101 | imageSize=(256, 256), hasColor=True) 102 | 103 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 104 | self.assertEqual(ptSettings.initialRadius, 30) 105 | self.assertEqual(ptSettings.finalRadius, 100) 106 | self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) 107 | self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) 108 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 109 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled3.shape[-3:-1]) 110 | 111 | np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled3) 112 | 113 | def test_IFRadiusAngleScaled(self): 114 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, 115 | initialRadius=30, finalRadius=100, 116 | initialAngle=2 / 4 * np.pi, 117 | finalAngle=5 / 4 * np.pi, 118 | imageSize=(256, 256), center=(128, 128), 119 | hasColor=True) 120 | 121 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 122 | self.assertEqual(ptSettings.initialRadius, 30) 123 | self.assertEqual(ptSettings.finalRadius, 100) 124 | self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) 125 | self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) 126 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 127 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled.shape[-3:-1]) 128 | 129 | np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled) 130 | 131 | def test_settings(self): 132 | cartesianImage1, ptSettings1 = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, 133 | initialRadius=30, finalRadius=100, 134 | initialAngle=2 / 4 * np.pi, 135 | finalAngle=5 / 4 * np.pi, 136 | imageSize=(256, 256), center=(128, 128), 137 | hasColor=True) 138 | 139 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled, 140 | settings=ptSettings1) 141 | 142 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 143 | self.assertEqual(ptSettings.initialRadius, 30) 144 | self.assertEqual(ptSettings.finalRadius, 100) 145 | self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) 146 | self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) 147 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 148 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesPolarImage_scaled.shape[-3:-1]) 149 | 150 | np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImage_scaled) 151 | 152 | def test_centerOrientationsWithImageSize(self): 153 | orientations = [ 154 | ('bottom-left', np.array([0, 0]), [0, 128], [0, 128], [128, 256], [128, 256]), 155 | ('bottom-middle', np.array([128, 0]), [0, 128], [0, 256], [128, 256], [0, 256]), 156 | ('bottom-right', np.array([256, 0]), [0, 128], [128, 256], [128, 256], [0, 128]), 157 | 158 | ('middle-left', np.array([0, 128]), [0, 256], [0, 128], [0, 256], [128, 256]), 159 | ('middle-middle', np.array([128, 128]), [0, 256], [0, 256], [0, 256], [0, 256]), 160 | ('middle-right', np.array([256, 128]), [0, 256], [128, 256], [0, 256], [0, 128]), 161 | 162 | ('top-left', np.array([0, 256]), [128, 256], [0, 128], [0, 128], [128, 256]), 163 | ('top-middle', np.array([128, 256]), [128, 256], [0, 256], [0, 128], [0, 256]), 164 | ('top-right', np.array([256, 256]), [128, 256], [128, 256], [0, 128], [0, 128]) 165 | ] 166 | 167 | for row in orientations: 168 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, 169 | center=row[0], imageSize=(256, 256), 170 | initialRadius=30, finalRadius=100, 171 | hasColor=True) 172 | 173 | np.testing.assert_array_equal(ptSettings.center, row[1]) 174 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 175 | 176 | np.testing.assert_almost_equal(cartesianImage[row[2][0]:row[2][1], row[3][0]:row[3][1], :], 177 | self.verticalLinesCartesianImage_scaled2[row[4][0]:row[4][1], 178 | row[5][0]:row[5][1], :]) 179 | 180 | def test_centerOrientationsWithoutImageSize(self): 181 | orientations = [ 182 | ('bottom-left', (100, 100), np.array([0, 0]), [128, 228], [128, 228]), 183 | ('bottom-middle', (100, 200), np.array([100, 0]), [128, 228], [28, 228]), 184 | ('bottom-right', (100, 100), np.array([100, 0]), [128, 228], [28, 128]), 185 | 186 | ('middle-left', (200, 100), np.array([0, 100]), [28, 228], [128, 228]), 187 | ('middle-middle', (200, 200), np.array([100, 100]), [28, 228], [28, 228]), 188 | ('middle-right', (200, 100), np.array([100, 100]), [28, 228], [28, 128]), 189 | 190 | ('top-left', (100, 100), np.array([0, 100]), [28, 128], [128, 228]), 191 | ('top-middle', (100, 200), np.array([100, 100]), [28, 128], [28, 228]), 192 | ('top-right', (100, 100), np.array([100, 100]), [28, 128], [28, 128]) 193 | ] 194 | 195 | for row in orientations: 196 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesPolarImage_scaled2, 197 | center=row[0], initialRadius=30, 198 | finalRadius=100, hasColor=True) 199 | 200 | self.assertEqual(ptSettings.cartesianImageSize, row[1]) 201 | np.testing.assert_array_equal(ptSettings.center, row[2]) 202 | 203 | np.testing.assert_almost_equal(cartesianImage, 204 | self.verticalLinesCartesianImage_scaled2[row[3][0]:row[3][1], 205 | row[4][0]:row[4][1], :]) 206 | 207 | def test_default_horizontalLines(self): 208 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.horizontalLinesPolarImage, 209 | center=(512, 384), imageSize=(768, 1024), 210 | finalRadius=640) 211 | 212 | np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) 213 | self.assertEqual(ptSettings.initialRadius, 0) 214 | self.assertEqual(ptSettings.finalRadius, 640) 215 | self.assertEqual(ptSettings.initialAngle, 0.0) 216 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 217 | self.assertEqual(ptSettings.cartesianImageSize, (768, 1024)) 218 | self.assertEqual(ptSettings.polarImageSize, self.horizontalLinesPolarImage.shape) 219 | 220 | assert_image_approx_equal_average(cartesianImage, self.horizontalLinesImage, 5) 221 | 222 | def test_3d_support_rgb(self): 223 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesAnimatedPolar, 224 | center=(128, 128), imageSize=(256, 256), 225 | finalRadius=182, hasColor=True) 226 | 227 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 228 | self.assertEqual(ptSettings.initialRadius, 0) 229 | self.assertEqual(ptSettings.finalRadius, 182) 230 | self.assertEqual(ptSettings.initialAngle, 0.0) 231 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 232 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 233 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesAnimatedPolar.shape[-3:-1]) 234 | 235 | assert_image_approx_equal_average(cartesianImage, self.verticalLinesAnimated, 10) 236 | 237 | def test_3d_support_rgb_multithreaded(self): 238 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.verticalLinesAnimatedPolar, 239 | center=(128, 128), imageSize=(256, 256), 240 | finalRadius=182, hasColor=True, 241 | useMultiThreading=True) 242 | 243 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 244 | self.assertEqual(ptSettings.initialRadius, 0) 245 | self.assertEqual(ptSettings.finalRadius, 182) 246 | self.assertEqual(ptSettings.initialAngle, 0.0) 247 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 248 | self.assertEqual(ptSettings.cartesianImageSize, (256, 256)) 249 | self.assertEqual(ptSettings.polarImageSize, self.verticalLinesAnimatedPolar.shape[-3:-1]) 250 | 251 | assert_image_approx_equal_average(cartesianImage, self.verticalLinesAnimated, 10) 252 | 253 | def test_3d_support_grayscale(self): 254 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.horizontalLinesAnimatedPolar, 255 | center=(512, 384), imageSize=(768, 1024), 256 | finalRadius=640) 257 | 258 | np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) 259 | self.assertEqual(ptSettings.initialRadius, 0) 260 | self.assertEqual(ptSettings.finalRadius, 640) 261 | self.assertEqual(ptSettings.initialAngle, 0.0) 262 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 263 | self.assertEqual(ptSettings.cartesianImageSize, (768, 1024)) 264 | self.assertEqual(ptSettings.polarImageSize, self.horizontalLinesAnimatedPolar.shape[-2:]) 265 | 266 | assert_image_approx_equal_average(cartesianImage, self.horizontalLinesAnimated, 10) 267 | 268 | def test_3d_support_grayscale_multithreaded(self): 269 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.horizontalLinesAnimatedPolar, 270 | center=(512, 384), imageSize=(768, 1024), 271 | finalRadius=640, useMultiThreading=True) 272 | 273 | np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) 274 | self.assertEqual(ptSettings.initialRadius, 0) 275 | self.assertEqual(ptSettings.finalRadius, 640) 276 | self.assertEqual(ptSettings.initialAngle, 0.0) 277 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 278 | self.assertEqual(ptSettings.cartesianImageSize, (768, 1024)) 279 | self.assertEqual(ptSettings.polarImageSize, self.horizontalLinesAnimatedPolar.shape[-2:]) 280 | 281 | assert_image_approx_equal_average(cartesianImage, self.horizontalLinesAnimated, 10) 282 | 283 | 284 | if __name__ == '__main__': 285 | unittest.main() 286 | -------------------------------------------------------------------------------- /polarTransform/tests/test_pointConversion.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | # Required specifically in each module so that searches happen at the parent directory for importing modules 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 7 | 8 | import polarTransform 9 | from polarTransform.tests.util import * 10 | 11 | 12 | class TestPointConversion(unittest.TestCase): 13 | def setUp(self): 14 | self.shortAxisApexImage = loadImage('shortAxisApex.png') 15 | self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') 16 | 17 | def test_polarConversion(self): 18 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, 19 | center=np.array([401, 365])) 20 | 21 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage([401, 365]), np.array([0, 0])) 22 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage([[401, 365], [401, 365]]), 23 | np.array([[0, 0], [0, 0]])) 24 | 25 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage((401, 365)), np.array([0, 0])) 26 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage(((401, 365), (401, 365))), 27 | np.array([[0, 0], [0, 0]])) 28 | 29 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage(np.array([401, 365])), np.array([0, 0])) 30 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage(np.array([[401, 365], [401, 365]])), 31 | np.array([[0, 0], [0, 0]])) 32 | 33 | # Fails here 34 | np.testing.assert_array_equal(ptSettings.getPolarPointsImage([[451, 365], [401, 400], [348, 365], [401, 305]]), 35 | np.array([[50 * 802 / 543, 0], [35 * 802 / 543, 400], [53 * 802 / 543, 800], 36 | [60 * 802 / 543, 1200]])) 37 | 38 | def test_cartesianConversion(self): 39 | cartesianImage, ptSettings = polarTransform.convertToCartesianImage(self.shortAxisApexPolarImage, 40 | center=[401, 365], imageSize=[608, 800], 41 | finalRadius=543) 42 | 43 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage([0, 0]), np.array([401, 365])) 44 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage([[0, 0], [0, 0]]), 45 | np.array([[401, 365], [401, 365]])) 46 | 47 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage((0, 0)), np.array([401, 365])) 48 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(((0, 0), (0, 0))), 49 | np.array([[401, 365], [401, 365]])) 50 | 51 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(np.array([0, 0])), np.array([401, 365])) 52 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage(np.array([[0, 0], [0, 0]])), 53 | np.array([[401, 365], [401, 365]])) 54 | 55 | np.testing.assert_array_equal(ptSettings.getCartesianPointsImage( 56 | np.array([[50 * 802 / 543, 0], [35 * 802 / 543, 400], [53 * 802 / 543, 800], 57 | [60 * 802 / 543, 1200]])), np.array([[451, 365], [401, 400], [348, 365], [401, 305]])) 58 | 59 | 60 | if __name__ == '__main__': 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /polarTransform/tests/test_polarCartesianConversion.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | # Required specifically in each module so that searches happen at the parent directory for importing modules 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 7 | 8 | import polarTransform 9 | from polarTransform.tests.util import * 10 | 11 | 12 | class TestPolarAndCartesianConversion(unittest.TestCase): 13 | def setUp(self): 14 | self.shortAxisApexImage = loadImage('shortAxisApex.png') 15 | self.verticalLinesImage = loadImage('verticalLines.png') 16 | 17 | self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') 18 | self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') 19 | self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') 20 | self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') 21 | self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') 22 | self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') 23 | 24 | self.verticalLinesCartesianImage_scaled = loadImage('verticalLinesCartesianImage_scaled.png') 25 | self.verticalLinesCartesianImage_scaled2 = loadImage('verticalLinesCartesianImage_scaled2.png') 26 | self.verticalLinesCartesianImage_scaled3 = loadImage('verticalLinesCartesianImage_scaled3.png') 27 | 28 | self.verticalLinesPolarImageBorders = loadImage('verticalLinesPolarImageBorders.png') 29 | self.verticalLinesCartesianImageBorders2 = loadImage('verticalLinesCartesianImageBorders2.png') 30 | self.verticalLinesPolarImageBorders3 = loadImage('verticalLinesPolarImageBorders3.png') 31 | self.verticalLinesCartesianImageBorders4 = loadImage('verticalLinesCartesianImageBorders4.png') 32 | 33 | def test_default(self): 34 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, 35 | center=np.array([401, 365])) 36 | 37 | cartesianImage = ptSettings.convertToCartesianImage(polarImage) 38 | 39 | np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) 40 | self.assertEqual(ptSettings.initialRadius, 0) 41 | self.assertEqual(ptSettings.finalRadius, 543) 42 | self.assertEqual(ptSettings.initialAngle, 0.0) 43 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 44 | self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) 45 | self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[-2:]) 46 | 47 | assert_image_approx_equal_average(cartesianImage, self.shortAxisApexImage, 5) 48 | 49 | def test_default2(self): 50 | polarImage1, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, 51 | center=np.array([401, 365]), radiusSize=2000, 52 | angleSize=4000) 53 | 54 | cartesianImage = ptSettings.convertToCartesianImage(polarImage1) 55 | ptSettings.polarImageSize = self.shortAxisApexPolarImage.shape[0:2] 56 | polarImage = ptSettings.convertToPolarImage(cartesianImage) 57 | 58 | np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) 59 | self.assertEqual(ptSettings.initialRadius, 0) 60 | self.assertEqual(ptSettings.finalRadius, 543) 61 | self.assertEqual(ptSettings.initialAngle, 0.0) 62 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 63 | self.assertEqual(ptSettings.cartesianImageSize, (608, 800)) 64 | self.assertEqual(ptSettings.polarImageSize, self.shortAxisApexPolarImage.shape[-2:]) 65 | 66 | assert_image_equal(polarImage, self.shortAxisApexPolarImage, 10) 67 | 68 | def test_borders(self): 69 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, hasColor=True, 70 | border='constant', borderVal=128.0) 71 | 72 | np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImageBorders) 73 | 74 | ptSettings.cartesianImageSize = (500, 500) 75 | ptSettings.center = np.array([250, 250]) 76 | cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='constant', borderVal=255.0) 77 | 78 | np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImageBorders2) 79 | 80 | def test_borders2(self): 81 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, hasColor=True, 82 | border='nearest') 83 | 84 | np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImageBorders3) 85 | 86 | ptSettings.cartesianImageSize = (500, 500) 87 | ptSettings.center = np.array([250, 250]) 88 | cartesianImage = ptSettings.convertToCartesianImage(polarImage, border='nearest') 89 | 90 | np.testing.assert_almost_equal(cartesianImage, self.verticalLinesCartesianImageBorders4) 91 | 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /polarTransform/tests/test_polarConversion.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | # Required specifically in each module so that searches happen at the parent directory for importing modules 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 7 | 8 | import polarTransform 9 | from polarTransform.tests.util import * 10 | 11 | 12 | class TestPolarConversion(unittest.TestCase): 13 | def setUp(self): 14 | self.shortAxisApexImage = loadImage('shortAxisApex.png') 15 | self.verticalLinesImage = loadImage('verticalLines.png') 16 | self.horizontalLinesImage = loadImage('horizontalLines.png', convertToGrayscale=True) 17 | 18 | self.shortAxisApexPolarImage = loadImage('shortAxisApexPolarImage.png') 19 | self.shortAxisApexPolarImage_centerMiddle = loadImage('shortAxisApexPolarImage_centerMiddle.png') 20 | self.verticalLinesPolarImage = loadImage('verticalLinesPolarImage.png') 21 | self.verticalLinesPolarImage_scaled = loadImage('verticalLinesPolarImage_scaled.png') 22 | self.verticalLinesPolarImage_scaled2 = loadImage('verticalLinesPolarImage_scaled2.png') 23 | self.verticalLinesPolarImage_scaled3 = loadImage('verticalLinesPolarImage_scaled3.png') 24 | 25 | self.horizontalLinesPolarImage = loadImage('horizontalLinesPolarImage.png', convertToGrayscale=True) 26 | 27 | self.verticalLinesAnimated = loadVideo('verticalLinesAnimated.avi') 28 | self.verticalLinesAnimatedPolar = loadVideo('verticalLinesAnimatedPolar.avi') 29 | self.horizontalLinesAnimated = loadVideo('horizontalLinesAnimated.avi', convertToGrayscale=True) 30 | self.horizontalLinesAnimatedPolar = loadVideo('horizontalLinesAnimatedPolar.avi', convertToGrayscale=True) 31 | 32 | def test_default(self): 33 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, 34 | center=np.array([401, 365])) 35 | 36 | np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) 37 | self.assertEqual(ptSettings.initialRadius, 0) 38 | self.assertEqual(ptSettings.finalRadius, 543) 39 | self.assertEqual(ptSettings.initialAngle, 0.0) 40 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 41 | self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) 42 | self.assertEqual(ptSettings.polarImageSize, (1600, 802)) 43 | 44 | np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) 45 | 46 | def test_final_radius(self): 47 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, 48 | center=np.array([350, 365])) 49 | 50 | np.testing.assert_array_equal(ptSettings.center, np.array([350, 365])) 51 | self.assertEqual(ptSettings.initialRadius, 0) 52 | self.assertEqual(ptSettings.finalRadius, 580) 53 | self.assertEqual(ptSettings.initialAngle, 0.0) 54 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 55 | self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) 56 | self.assertEqual(ptSettings.polarImageSize, (1600, 898)) 57 | 58 | def test_defaultCenter(self): 59 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage) 60 | 61 | np.testing.assert_array_equal(ptSettings.center, np.array([400, 304])) 62 | self.assertEqual(ptSettings.initialRadius, 0) 63 | self.assertEqual(ptSettings.finalRadius, 503) 64 | self.assertEqual(ptSettings.initialAngle, 0.0) 65 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 66 | self.assertEqual(ptSettings.cartesianImageSize, self.shortAxisApexImage.shape) 67 | self.assertEqual(ptSettings.polarImageSize, (1600, 800)) 68 | 69 | np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage_centerMiddle) 70 | 71 | def test_notNumpyArrayCenter(self): 72 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, center=(401, 365)) 73 | np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) 74 | np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) 75 | 76 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.shortAxisApexImage, center=(401, 365)) 77 | np.testing.assert_array_equal(ptSettings.center, np.array([401, 365])) 78 | np.testing.assert_almost_equal(polarImage, self.shortAxisApexPolarImage) 79 | 80 | def test_IFRadius(self): 81 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, 82 | finalRadius=100, hasColor=True) 83 | 84 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 85 | self.assertEqual(ptSettings.initialRadius, 30) 86 | self.assertEqual(ptSettings.finalRadius, 100) 87 | self.assertEqual(ptSettings.initialAngle, 0.0) 88 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 89 | self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[-3:-1]) 90 | self.assertEqual(ptSettings.polarImageSize, (1024, 99)) 91 | 92 | np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled2) 93 | 94 | def test_IFRadiusAngle(self): 95 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, 96 | finalRadius=100, initialAngle=2 / 4 * np.pi, 97 | finalAngle=5 / 4 * np.pi, hasColor=True) 98 | 99 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 100 | self.assertEqual(ptSettings.initialRadius, 30) 101 | self.assertEqual(ptSettings.finalRadius, 100) 102 | self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) 103 | self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) 104 | self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[-3:-1]) 105 | self.assertEqual(ptSettings.polarImageSize, (384, 99)) 106 | 107 | np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled3) 108 | 109 | def test_IFRadiusAngleScaled(self): 110 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, initialRadius=30, 111 | finalRadius=100, initialAngle=2 / 4 * np.pi, 112 | finalAngle=5 / 4 * np.pi, radiusSize=140, 113 | angleSize=700, hasColor=True) 114 | 115 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 116 | self.assertEqual(ptSettings.initialRadius, 30) 117 | self.assertEqual(ptSettings.finalRadius, 100) 118 | self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) 119 | self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) 120 | self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[-3:-1]) 121 | self.assertEqual(ptSettings.polarImageSize, (700, 140)) 122 | 123 | np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled) 124 | 125 | def test_settings(self): 126 | polarImage1, ptSettings1 = polarTransform.convertToPolarImage(self.verticalLinesImage, 127 | initialRadius=30, finalRadius=100, 128 | initialAngle=2 / 4 * np.pi, 129 | finalAngle=5 / 4 * np.pi, radiusSize=140, 130 | angleSize=700, hasColor=True) 131 | 132 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesImage, settings=ptSettings1) 133 | 134 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 135 | self.assertEqual(ptSettings.initialRadius, 30) 136 | self.assertEqual(ptSettings.finalRadius, 100) 137 | self.assertEqual(ptSettings.initialAngle, 2 / 4 * np.pi) 138 | self.assertEqual(ptSettings.finalAngle, 5 / 4 * np.pi) 139 | self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesImage.shape[-3:-1]) 140 | self.assertEqual(ptSettings.polarImageSize, (700, 140)) 141 | 142 | np.testing.assert_almost_equal(polarImage, self.verticalLinesPolarImage_scaled) 143 | 144 | polarImage2 = ptSettings1.convertToPolarImage(self.verticalLinesImage) 145 | np.testing.assert_almost_equal(polarImage2, self.verticalLinesPolarImage_scaled) 146 | 147 | def test_default_horizontalLines(self): 148 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.horizontalLinesImage) 149 | 150 | np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) 151 | self.assertEqual(ptSettings.initialRadius, 0) 152 | # sqrt(512^2 + 384^2) maximum distance = 640 153 | self.assertEqual(ptSettings.finalRadius, 640) 154 | self.assertEqual(ptSettings.initialAngle, 0.0) 155 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 156 | self.assertEqual(ptSettings.cartesianImageSize, self.horizontalLinesImage.shape) 157 | self.assertEqual(ptSettings.polarImageSize, (2048, 1024)) 158 | 159 | np.testing.assert_almost_equal(polarImage, self.horizontalLinesPolarImage) 160 | 161 | def test_3d_support_rgb(self): 162 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesAnimated, hasColor=True) 163 | 164 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 165 | self.assertEqual(ptSettings.initialRadius, 0) 166 | self.assertEqual(ptSettings.finalRadius, 182) 167 | self.assertEqual(ptSettings.initialAngle, 0.0) 168 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 169 | self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesAnimated.shape[-3:-1]) 170 | self.assertEqual(ptSettings.polarImageSize, (1024, 256)) 171 | 172 | np.testing.assert_almost_equal(polarImage, self.verticalLinesAnimatedPolar) 173 | 174 | def test_3d_support_rgb_multithreaded(self): 175 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.verticalLinesAnimated, hasColor=True, 176 | useMultiThreading=True) 177 | 178 | np.testing.assert_array_equal(ptSettings.center, np.array([128, 128])) 179 | self.assertEqual(ptSettings.initialRadius, 0) 180 | self.assertEqual(ptSettings.finalRadius, 182) 181 | self.assertEqual(ptSettings.initialAngle, 0.0) 182 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 183 | self.assertEqual(ptSettings.cartesianImageSize, self.verticalLinesAnimated.shape[-3:-1]) 184 | self.assertEqual(ptSettings.polarImageSize, (1024, 256)) 185 | 186 | np.testing.assert_almost_equal(polarImage, self.verticalLinesAnimatedPolar) 187 | 188 | def test_3d_support_grayscale(self): 189 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.horizontalLinesAnimated) 190 | 191 | np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) 192 | self.assertEqual(ptSettings.initialRadius, 0) 193 | self.assertEqual(ptSettings.finalRadius, 640) 194 | self.assertEqual(ptSettings.initialAngle, 0.0) 195 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 196 | self.assertEqual(ptSettings.cartesianImageSize, self.horizontalLinesAnimated.shape[-2:]) 197 | self.assertEqual(ptSettings.polarImageSize, (2048, 1024)) 198 | 199 | np.testing.assert_almost_equal(polarImage, self.horizontalLinesAnimatedPolar) 200 | 201 | def test_3d_support_grayscale_multithreaded(self): 202 | polarImage, ptSettings = polarTransform.convertToPolarImage(self.horizontalLinesAnimated, 203 | useMultiThreading=True) 204 | 205 | np.testing.assert_array_equal(ptSettings.center, np.array([512, 384])) 206 | self.assertEqual(ptSettings.initialRadius, 0) 207 | self.assertEqual(ptSettings.finalRadius, 640) 208 | self.assertEqual(ptSettings.initialAngle, 0.0) 209 | self.assertEqual(ptSettings.finalAngle, 2 * np.pi) 210 | self.assertEqual(ptSettings.cartesianImageSize, self.horizontalLinesAnimated.shape[-2:]) 211 | self.assertEqual(ptSettings.polarImageSize, (2048, 1024)) 212 | 213 | np.testing.assert_almost_equal(polarImage, self.horizontalLinesAnimatedPolar) 214 | 215 | 216 | if __name__ == '__main__': 217 | unittest.main() 218 | -------------------------------------------------------------------------------- /polarTransform/tests/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import cv2 4 | import imageio 5 | import numpy as np 6 | 7 | dataDirectory = os.path.join(os.path.dirname(__file__), 'data') 8 | 9 | 10 | def loadImage(filename, flipud=True, convertToGrayscale=False): 11 | image = imageio.imread(os.path.join(dataDirectory, filename), ignoregamma=True) 12 | 13 | if convertToGrayscale and image.ndim == 3: 14 | image = image[:, :, 0] 15 | elif image.ndim == 3 and image.shape[-1] == 4: 16 | image = image[:, :, 0:3] 17 | 18 | return np.flipud(image) if flipud else image 19 | 20 | 21 | def loadVideo(filename, flipud=True, convertToGrayscale=False): 22 | capture = cv2.VideoCapture(os.path.join(dataDirectory, filename)) 23 | frames = [] 24 | 25 | while capture.isOpened(): 26 | # Read frame 27 | returnValue, frame = capture.read() 28 | 29 | # Error reading image, move on 30 | if not returnValue: 31 | break 32 | 33 | # Convert to grayscale or remove alpha component if RGB image 34 | if convertToGrayscale and frame.ndim == 3: 35 | frame = frame[:, :, 0] 36 | elif frame.ndim == 3 and frame.shape[-1] == 4: 37 | frame = frame[:, :, 0:3] 38 | 39 | frames.append(frame) 40 | 41 | # Combine the video on the last axis 42 | image3D = np.stack(frames, axis=0) 43 | 44 | if flipud: 45 | image3D = np.flip(image3D, axis=1) 46 | 47 | return image3D 48 | 49 | 50 | def saveImage(filename, image, flipud=True): 51 | imageio.imwrite(os.path.join(dataDirectory, filename), np.flipud(image) if flipud else image) 52 | 53 | 54 | def saveVideo(filename, image, fps=20, fourcc='MJLS', flipud=True): 55 | # Assuming color is present if four dimensions are available 56 | hasColor = image.ndim == 4 57 | 58 | # Construct four character code 59 | if isinstance(fourcc, str): 60 | fourcc = cv2.VideoWriter_fourcc(*fourcc) 61 | 62 | # Retrieve the 2D image shape 63 | # If there is color channels, get 2nd and 3rd to last dimensions, otherwise get last 2 dimensions 64 | imageShape = image.shape[-2:-4:-1] if hasColor else image.shape[-1:-3:-1] 65 | 66 | # Construct codec to use and create writer 67 | # MJLS is one of the few FFMPEG formats that supports lossy encoding in OpenCV specifically because it does not 68 | # convert to YUV color space 69 | writer = cv2.VideoWriter(os.path.join(dataDirectory, filename), fourcc, fps, imageShape, isColor=hasColor) 70 | 71 | # Flip image if specified 72 | if flipud: 73 | image = np.flip(image, axis=1) 74 | 75 | # Write frames 76 | for slice in image: 77 | writer.write(slice) 78 | 79 | # Finish writing 80 | writer.release() 81 | 82 | 83 | def assert_image_equal(desired, actual, diff): 84 | difference = np.abs(desired.astype(int) - actual.astype(int)).astype(np.uint8) 85 | 86 | assert (np.all(difference <= diff)) 87 | 88 | 89 | def assert_image_approx_equal_average(desired, actual, averageDiff, hasColor=False): 90 | assert desired.ndim == actual.ndim, 'Images are not equal, difference in dimensions: %i != %i' % \ 91 | (desired.ndim, actual.ndim) 92 | assert desired.shape == actual.shape, 'Images are not equal, difference in shape %s != %s' % \ 93 | (desired.shape, actual.shape) 94 | 95 | # Calculate the difference between the two images 96 | difference = np.abs(desired.astype(int) - actual.astype(int)).astype(np.uint8) 97 | 98 | axes = (-2, -3) if hasColor else (-1, -2) 99 | 100 | # Get average difference between each different pixel 101 | # averageDiffPerPixel = np.sum(difference, axis=(0, 1)) / np.sum(difference > 0, axis=(0, 1)) 102 | averageDiffPerPixel = np.sum(difference, axis=axes) / np.sum(difference > 0, axis=axes) 103 | 104 | assert np.all(averageDiffPerPixel < averageDiff), 'Images are not equal, average difference between each channel ' \ 105 | 'is not less than the given threshold, %s < %s' % \ 106 | (averageDiffPerPixel, averageDiff) 107 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | opencv-python 2 | sphinx-rtd-theme 3 | codecov -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | scikit-image 4 | numpydoc 5 | imageio -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | from polarTransform._version import __version__ 6 | 7 | currentPath = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | # Get the long description from the README file 10 | with open(os.path.join(currentPath, 'README.rst'), 'r') as f: 11 | long_description = f.read() 12 | 13 | long_description = '\n' + long_description 14 | setup(name='polarTransform', 15 | version=__version__, 16 | description='Library that can converts between polar and cartesian domain with images and individual points.', 17 | long_description=long_description, 18 | long_description_content_type='text/x-rst', 19 | author='Addison Elliott', 20 | author_email='addison.elliott@gmail.com', 21 | url='https://github.com/addisonElliott/polarTransform', 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Topic :: Scientific/Engineering', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.4', 28 | 'Programming Language :: Python :: 3.5', 29 | 'Programming Language :: Python :: 3.6' 30 | ], 31 | keywords='polar transform cartesian conversion logPolar linearPolar cv2 opencv radius theta angle image images', 32 | project_urls={ 33 | 'Documentation': 'http://polartransform.readthedocs.io', 34 | 'Source': 'https://github.com/addisonElliott/polarTransform', 35 | 'Tracker': 'https://github.com/addisonElliott/polarTransform/issues', 36 | }, 37 | python_requires='>=3', 38 | packages=find_packages(), 39 | include_package_data=True, 40 | license='MIT License', 41 | install_requires=[ 42 | 'numpy', 'scipy', 'scikit-image'] 43 | ) 44 | --------------------------------------------------------------------------------