├── .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 |
--------------------------------------------------------------------------------