├── test ├── __init__.py ├── test_convert_eulers.py ├── test_matrix2euler_intrinsic.py ├── test_euler2euler.py ├── test_matrix2euler_extrinsic.py ├── test_euler2matrix.py └── test_matrix2euler.py ├── .gitignore ├── eulerangles ├── math │ ├── rotation_matrices │ │ ├── __init__.py │ │ ├── utils.py │ │ ├── angle_to_matrix.py │ │ └── rotation_matrix_composition.py │ ├── constants.py │ ├── __init__.py │ ├── eulers_to_rotation_matrix.py │ ├── eulers_to_eulers.py │ └── rotation_matrix_to_eulers.py ├── version.py ├── __init__.py ├── utils.py ├── constants.py ├── base.py └── interface.py ├── requirements.txt ├── docs ├── requirements.txt ├── source │ ├── api.rst │ ├── index.rst │ ├── usage │ │ ├── installation.md │ │ └── quick_start.md │ └── conf.py ├── Makefile └── make.bat ├── setup.py ├── .travis.yml ├── LICENSE ├── setup.cfg ├── .github └── workflows │ └── test_and_deploy.yml └── README.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | -------------------------------------------------------------------------------- /eulerangles/math/rotation_matrices/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eulerangles/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.2' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.16 2 | dataclasses; python_version < '3.7' 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | alabaster 3 | sphinx 4 | numpydoc 5 | recommonmark 6 | 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup(use_scm_version={"write_to": "eulerangles/_version.py"}) 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install pytest 10 | 11 | script: 12 | - pytest 13 | -------------------------------------------------------------------------------- /eulerangles/math/rotation_matrices/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def invert_rotation_matrices(rotation_matrices: np.ndarray): 5 | """ 6 | Invert rotation matrices by transposing the last two axes 7 | """ 8 | return rotation_matrices.swapaxes(-1, -2) 9 | 10 | 11 | -------------------------------------------------------------------------------- /eulerangles/math/constants.py: -------------------------------------------------------------------------------- 1 | valid_axes = ( 2 | 'xyz', 3 | 'xyx', 4 | 'xzx', 5 | 'xzy', 6 | 'yxy', 7 | 'yxz', 8 | 'yzx', 9 | 'yzy', 10 | 'zxy', 11 | 'zxz', 12 | 'zyx', 13 | 'zyz', 14 | ) 15 | 16 | valid_matrix_composition_modes = ( 17 | 'intrinsic', 18 | 'extrinsic' 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /eulerangles/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ConversionMeta 2 | from .interface import convert_eulers 3 | from .math.eulers_to_eulers import euler2euler 4 | from .math.rotation_matrix_to_eulers import matrix2euler 5 | from .math.eulers_to_rotation_matrix import euler2matrix 6 | from .math.rotation_matrices.utils import invert_rotation_matrices 7 | from .version import __version__ 8 | -------------------------------------------------------------------------------- /eulerangles/math/__init__.py: -------------------------------------------------------------------------------- 1 | from .rotation_matrix_to_eulers import matrix2euler_right_handed, matrix2xyx_extrinsic, matrix2xyz_extrinsic, \ 2 | matrix2xzx_extrinsic, matrix2xzy_extrinsic, matrix2yxy_extrinsic, matrix2yxz_extrinsic, \ 3 | matrix2yzx_extrinsic, matrix2yzy_extrinsic, matrix2zxy_extrinsic, matrix2zxz_extrinsic, \ 4 | matrix2zyx_extrinsic, matrix2zyz_extrinsic 5 | 6 | -------------------------------------------------------------------------------- /eulerangles/utils.py: -------------------------------------------------------------------------------- 1 | from .constants import euler_angle_metadata 2 | 3 | 4 | def get_conversion_metadata(convention): 5 | """ 6 | Attempts to retrieve ConversionMeta objects from a given software package name 7 | """ 8 | try: 9 | convention = convention.strip().lower() 10 | convention = euler_angle_metadata[convention] 11 | return convention 12 | 13 | except KeyError: 14 | raise NotImplementedError(f"Convention '{convention}' is not yet implemented, " 15 | f"please create your own EulerAngleConvention object") 16 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | euler2matrix 8 | ------------ 9 | 10 | .. autofunction:: eulerangles.euler2matrix 11 | 12 | matrix2euler 13 | ------------ 14 | 15 | .. autofunction:: eulerangles.matrix2euler 16 | 17 | euler2euler 18 | ----------- 19 | 20 | .. autofunction:: eulerangles.euler2euler 21 | 22 | convert_eulers 23 | -------------- 24 | .. autofunction:: eulerangles.convert_eulers 25 | 26 | invert_rotation_matrices 27 | ------------------------ 28 | .. autofunction:: eulerangles.invert_rotation_matrices 29 | 30 | ConversionMeta 31 | -------------- 32 | .. autoclass:: eulerangles.ConversionMeta 33 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 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) 21 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | The eulerangles documentation 2 | ======================================= 3 | 4 | `Euler angles `_ are often used to represent rigid body rotations in 3D. 5 | 6 | These transformations can be defined in many different ways. 7 | The world of transformations is filled with 8 | `ambiguities `_ which can make it harder 9 | than necessary to interface softwares which define their transformations differently. 10 | 11 | This package is designed to simplify the handling of large sets of 12 | Euler angles in Python. 13 | 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | usage/quick_start 19 | usage/installation 20 | api 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/test_convert_eulers.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_array_almost_equal 2 | 3 | from eulerangles import convert_eulers 4 | 5 | dynamo_eulers = [-47.2730, 1.1777, -132.3000] 6 | relion_eulers = [137.7000, 1.1777, 42.7270] 7 | 8 | 9 | def test_convert_eulers_dynamo_to_relion(): 10 | result_eulers = convert_eulers(dynamo_eulers, 11 | source_meta='dynamo', 12 | target_meta='relion') 13 | assert_array_almost_equal(relion_eulers, result_eulers, decimal=5) 14 | 15 | 16 | def test_convert_eulers_relion_to_dynamo(): 17 | result_eulers = convert_eulers(relion_eulers, 18 | source_meta='relion', 19 | target_meta='dynamo') 20 | assert_array_almost_equal(dynamo_eulers, result_eulers, decimal=5) 21 | -------------------------------------------------------------------------------- /test/test_matrix2euler_intrinsic.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_array_almost_equal 2 | 3 | from eulerangles import euler2matrix, matrix2euler 4 | from eulerangles.math.constants import valid_axes 5 | 6 | 7 | def test_all_valid_axes(): 8 | eulers = [10, 20, 30] 9 | for axes in valid_axes: 10 | rotation_matrix = euler2matrix(eulers, axes=axes, intrinsic=True, right_handed_rotation=True) 11 | 12 | result_eulers = matrix2euler(rotation_matrix, axes=axes, right_handed_rotation=True, 13 | intrinsic=True) 14 | result_rotation_matrix = euler2matrix(result_eulers, axes=axes, intrinsic=True, 15 | right_handed_rotation=True) 16 | 17 | assert_array_almost_equal(eulers, result_eulers) 18 | assert_array_almost_equal(rotation_matrix, result_rotation_matrix) 19 | -------------------------------------------------------------------------------- /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=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /eulerangles/constants.py: -------------------------------------------------------------------------------- 1 | from .base import ConversionMeta 2 | 3 | euler_angle_metadata = { 4 | 'relion': ConversionMeta(name='relion', 5 | axes='zyz', 6 | intrinsic=True, 7 | right_handed_rotation=True, 8 | active=False), 9 | 10 | 'dynamo': ConversionMeta(name='dynamo', 11 | axes='zxz', 12 | intrinsic=False, 13 | right_handed_rotation=True, 14 | active=False), 15 | 16 | 'warp': ConversionMeta(name='warp', 17 | axes='ZYZ', 18 | intrinsic=True, 19 | right_handed_rotation=True, 20 | active=False), 21 | 22 | 'm': ConversionMeta(name='warp', 23 | axes='ZYZ', 24 | intrinsic=True, 25 | right_handed_rotation=True, 26 | active=False), 27 | } 28 | -------------------------------------------------------------------------------- /test/test_euler2euler.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_array_almost_equal 2 | 3 | from eulerangles import euler2euler 4 | from eulerangles.utils import get_conversion_metadata 5 | 6 | 7 | def test_euler2euler_dynamo2relion(): 8 | dynamo_eulers = [-47.2730, 1.1777, -132.3000] 9 | relion_eulers = [137.7000, 1.1777, 42.7270] 10 | 11 | dynamo_meta = get_conversion_metadata('dynamo') 12 | relion_meta = get_conversion_metadata('relion') 13 | 14 | result_eulers = euler2euler(dynamo_eulers, 15 | source_axes=dynamo_meta.axes, 16 | source_intrinsic=dynamo_meta.intrinsic, 17 | source_right_handed_rotation=dynamo_meta.right_handed_rotation, 18 | target_axes=relion_meta.axes, 19 | target_intrinsic=relion_meta.intrinsic, 20 | target_right_handed_rotation=relion_meta.right_handed_rotation, 21 | invert_matrix=False) 22 | 23 | assert_array_almost_equal(relion_eulers, result_eulers, decimal=5) 24 | -------------------------------------------------------------------------------- /test/test_matrix2euler_extrinsic.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_array_almost_equal 2 | 3 | from eulerangles import euler2matrix, matrix2euler 4 | from eulerangles.math.constants import valid_axes 5 | 6 | 7 | def test_all_valid_axes(): 8 | eulers = [10, 20, 30] 9 | for axes in valid_axes: 10 | rotation_matrix = euler2matrix(eulers, 11 | axes=axes, 12 | intrinsic=False, 13 | right_handed_rotation=True) 14 | 15 | result_eulers = matrix2euler(rotation_matrix, 16 | axes=axes, 17 | right_handed_rotation=True, 18 | intrinsic=False) 19 | result_rotation_matrix = euler2matrix(result_eulers, 20 | axes=axes, 21 | intrinsic=False, 22 | right_handed_rotation=True) 23 | 24 | assert_array_almost_equal(eulers, result_eulers) 25 | assert_array_almost_equal(rotation_matrix, result_rotation_matrix) 26 | -------------------------------------------------------------------------------- /docs/source/usage/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | `eulerangles` is available through [PyPI](https://pypi.org/project/eulerangles/). 3 | 4 | The latest version of the package can be installed with `pip` 5 | 6 | ``` 7 | pip install eulerangles 8 | ``` 9 | 10 | ## Dependencies 11 | `eulerangles` depends on [Numpy](https://numpy.org/) for fast, vectorised mathematical operations. 12 | 13 | ## A note about virtual environments 14 | One Python installation may not be able to simultaneously meet the requirements of many interdependent packages, 15 | some of which may require newer and older versions of the same package. 16 | 17 | To get around this issue, it is recommended to work within virtual environments. 18 | Virtual environments provide an isolated Python installation with a set of required packages for a given project, 19 | environments can be activated and deactivated at will. 20 | 21 | Virtual environments can be managed with 22 | [venv](https://docs.python.org/3/tutorial/venv.html) 23 | or 24 | [conda](https://docs.conda.io/en/latest/). 25 | 26 | A complete conda installation containing many packages for data science is provided by 27 | [Anaconda](https://www.anaconda.com/products/individual). 28 | A lightweight version [Miniconda](https://docs.conda.io/en/latest/miniconda.html) is also available. 29 | 30 | The author of this package uses `conda` (via Miniconda) to manage software environments. 31 | -------------------------------------------------------------------------------- /eulerangles/base.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class ConversionMeta: 6 | """ 7 | A simple object holding metadata explaining how to interpret Euler angles in the context of 8 | density reconstruction from transmission electron microscopy data. 9 | 10 | name: str 11 | the name of the software package 12 | axes: str 13 | a valid non-sequential sequence of axes e.g. 'zxz', 'yxz' 14 | intrinsic: bool 15 | True - the euler angles represent intrinsic rotations, the coordinate system moves with 16 | the rotating rigid body 17 | False - the euler angles represent extrinsic rotations, the rigid body rotates with 18 | respect to a fixed coordinate system 19 | right_handed_rotation: bool 20 | True - the euler angles represent right handed rotations in a right handed coordinate system 21 | False - the euler angles represent left handed rotations in a right handed coordinate system 22 | active: bool 23 | True - the transformation is an active transformation 24 | False - the transformation is a passive transformation 25 | this property is only used during comparison of ConversionMeta objects when 26 | deciding whether or not to invert rotation matrices during euler angle conversion. 27 | """ 28 | name: str 29 | axes: str 30 | intrinsic: bool 31 | right_handed_rotation: bool 32 | active: bool 33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Alister Burt 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = eulerangles 3 | description = deal with large sets of Euler angles in Python 4 | long_description = file: README.md 5 | long_description_content_type = text/markdown 6 | url = https://github.com/alisterburt/eulerangles 7 | author = Alister Burt 8 | author_email = alisterburt@gmail.com 9 | license = BSD-3-Clause 10 | license_file = LICENSE 11 | classifiers = 12 | Development Status :: 2 - Pre-Alpha 13 | License :: OSI Approved :: BSD License 14 | Natural Language :: English 15 | Programming Language :: Python :: 3 16 | Programming Language :: Python :: 3 :: Only 17 | Programming Language :: Python :: 3.8 18 | Programming Language :: Python :: 3.9 19 | Programming Language :: Python :: 3.10 20 | 21 | project_urls = 22 | Source Code =https://github.com/alisterburt/eulerangles 23 | 24 | [options] 25 | packages = find: 26 | install_requires = 27 | numpy 28 | python_requires = >=3.8 29 | setup_requires = 30 | setuptools_scm 31 | zip_safe = False 32 | 33 | [options.extras_require] 34 | dev = 35 | black 36 | flake8 37 | flake8-docstrings 38 | ipython 39 | isort 40 | jedi<0.18.0 41 | jupyter-book 42 | mypy 43 | pre-commit 44 | pydocstyle 45 | pytest 46 | testing = 47 | pytest 48 | 49 | [bdist_wheel] 50 | universal = 1 51 | 52 | [flake8] 53 | exclude = docs,_version.py,.eggs,examples 54 | max-line-length = 88 55 | docstring-convention = numpy 56 | ignore = D100, D213, D401, D413, D107, W503 57 | 58 | [isort] 59 | profile = black 60 | src_paths = eulerangles 61 | 62 | [pydocstyle] 63 | match_dir = eulerangles 64 | convention = numpy 65 | add_select = D402,D415,D417 66 | ignore = D100, D213, D401, D413, D107 67 | 68 | [tool:pytest] 69 | addopts = -W error 70 | 71 | [mypy] 72 | files = eulerangles 73 | warn_unused_configs = True 74 | warn_unused_ignores = True 75 | check_untyped_defs = True 76 | implicit_reexport = False 77 | show_column_numbers = True 78 | show_error_codes = True 79 | ignore_missing_imports = True 80 | -------------------------------------------------------------------------------- /.github/workflows/test_and_deploy.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | tags: 9 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 10 | pull_request: 11 | branches: 12 | - master 13 | - main 14 | workflow_dispatch: 15 | 16 | jobs: 17 | test: 18 | name: ${{ matrix.platform }} py${{ matrix.python-version }} 19 | runs-on: ${{ matrix.platform }} 20 | strategy: 21 | matrix: 22 | platform: [ ubuntu-latest ] 23 | python-version: [ "3.8", "3.9", "3.10" ] 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip 36 | pip install setuptools pytest 37 | pip install -e . 38 | 39 | - name: Test with pytest 40 | run: pytest -W "ignore::DeprecationWarning" 41 | env: 42 | PLATFORM: ${{ matrix.platform }} 43 | 44 | - name: Coverage 45 | uses: codecov/codecov-action@v1 46 | 47 | deploy: 48 | # this will run when you have tagged a commit, starting with "v*" 49 | # and requires that you have put your twine API key in your 50 | # github secrets (see readme for details) 51 | needs: [ test ] 52 | runs-on: ubuntu-latest 53 | if: contains(github.ref, 'tags') 54 | steps: 55 | - uses: actions/checkout@v2 56 | - name: Set up Python 57 | uses: actions/setup-python@v2 58 | with: 59 | python-version: "3.x" 60 | - name: Install dependencies 61 | run: | 62 | python -m pip install --upgrade pip 63 | pip install -U setuptools setuptools_scm wheel twine 64 | - name: Build and publish 65 | env: 66 | TWINE_USERNAME: __token__ 67 | TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }} 68 | run: | 69 | git tag 70 | python setup.py sdist bdist_wheel 71 | twine upload dist/* 72 | -------------------------------------------------------------------------------- /eulerangles/math/eulers_to_rotation_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .rotation_matrices.angle_to_matrix import theta2rotm 4 | from .rotation_matrices.rotation_matrix_composition import compose_rotation_matrices 5 | from .constants import valid_axes 6 | 7 | 8 | def euler2matrix(euler_angles: np.ndarray, 9 | axes: str, 10 | intrinsic: bool, 11 | right_handed_rotation: bool) -> np.ndarray: 12 | """ 13 | Derive rotation matrices from a set of euler angles. 14 | 15 | Parameters 16 | ---------- 17 | euler_angles : (n, 3) or (3,) array 18 | euler angles (in degrees) 19 | axes : str 20 | valid sequence of three non-sequential axes from 'x', 'y' and 'z' 21 | e.g. 'zyz', 'zxz', 'xyz' 22 | intrinsic : bool 23 | True - Euler angles are interpreted as intrinsic rotations 24 | False - Euler angles are interpreted as extrinsic rotations 25 | right_handed_rotation : bool 26 | True - Euler angles are interpreted as right handed rotations 27 | False - Euler angles are interpreted as left handed rotations 28 | 29 | Returns 30 | ------- 31 | rotation_matrices : (n, 3, 3) or (3, 3) array 32 | rotation matrices derived from euler angles. 33 | 34 | """ 35 | # Check and santise input 36 | euler_angles = np.asarray(euler_angles).reshape((-1, 3)) 37 | axes_sanitised = axes.strip().lower() 38 | 39 | if axes_sanitised not in valid_axes: 40 | raise ValueError(f'Axes {axes} are not a valid set of euler angle axes') 41 | 42 | axes = axes_sanitised 43 | 44 | # Calculate elemental rotation matrices from euler angles 45 | if not right_handed_rotation: 46 | # Left handed rotation case 47 | euler_angles = euler_angles * -1 48 | 49 | elemental_rotations = [theta2rotm(theta=euler_angles[:, idx], axis=axes[idx]) 50 | for idx in range(3)] 51 | 52 | # Compose final rotation matrices from elemental rotation matrices 53 | if intrinsic: 54 | mode = 'intrinsic' 55 | else: 56 | mode = 'extrinsic' 57 | 58 | rotation_matrices = compose_rotation_matrices(elemental_rotations, mode=mode) 59 | 60 | return rotation_matrices.squeeze() 61 | -------------------------------------------------------------------------------- /eulerangles/interface.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import numpy as np 4 | from .base import ConversionMeta 5 | from .math.eulers_to_eulers import euler2euler 6 | from .utils import get_conversion_metadata 7 | 8 | 9 | def convert_eulers(euler_angles: np.ndarray, 10 | source_meta: Union[ConversionMeta, str], 11 | target_meta: Union[ConversionMeta, str]): 12 | """ 13 | Convert Euler angles defined according to one 'convention' into Euler angles defined 14 | according to another. 15 | 16 | Parameters 17 | ---------- 18 | euler_angles : (n, 3) or (3,) array of float 19 | Euler angles to be converted 20 | 21 | source_meta : ConversionMeta or str 22 | metadata defining how to interpret the euler angles or a string with the name of a 23 | software package 24 | 25 | target_meta : ConversionMeta or str 26 | metadata defining how to generate euler angles or a string with the name of a software 27 | package 28 | 29 | Returns 30 | ------- 31 | euler_angles : (n, 3) or (3,) array of float 32 | Euler angles resulting from conversion 33 | """ 34 | # Attempt to get appropriate conversion metadata if a software package name is provided 35 | if isinstance(source_meta, str): 36 | source_meta = get_conversion_metadata(source_meta) 37 | if isinstance(target_meta, str): 38 | target_meta = get_conversion_metadata(target_meta) 39 | 40 | # Check if desired transformation is of the same type as the input Eulers 41 | if source_meta.active != target_meta.active: 42 | invert_matrix = True 43 | else: 44 | invert_matrix = False 45 | 46 | # Convert euler angles according to metadata 47 | final_eulers = euler2euler(euler_angles, 48 | source_axes=source_meta.axes, 49 | source_intrinsic=source_meta.intrinsic, 50 | source_right_handed_rotation=source_meta.right_handed_rotation, 51 | target_axes=target_meta.axes, 52 | target_intrinsic=target_meta.intrinsic, 53 | target_right_handed_rotation=target_meta.right_handed_rotation, 54 | invert_matrix=invert_matrix) 55 | 56 | return final_eulers 57 | 58 | -------------------------------------------------------------------------------- /test/test_euler2matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_almost_equal 3 | import pytest 4 | 5 | from eulerangles import euler2matrix 6 | from eulerangles.utils import get_conversion_metadata 7 | from eulerangles.math.constants import valid_axes 8 | 9 | test_eulers_single = [10, 20, 30] 10 | test_eulers_multiple = np.arange(15).reshape((5, 3)) 11 | 12 | 13 | def test_euler2matrix_all_combinations(): 14 | eulers = (test_eulers_single, test_eulers_multiple) 15 | for eulers in eulers: 16 | for axes in valid_axes: 17 | for intrinsic in (True, False): 18 | for right_handed_rotation in (True, False): 19 | matrices = euler2matrix(eulers, 20 | axes=axes, 21 | intrinsic=intrinsic, 22 | right_handed_rotation=right_handed_rotation) 23 | assert matrices.shape[-1] == 3 24 | assert matrices.shape[-2] == 3 25 | 26 | 27 | def test_euler2matrix_dynamo(): 28 | dynamo_eulers = np.array([30, 60, 75]) 29 | dynamo_matrix = np.array([[-0.0173, -0.5477, 0.8365], 30 | [0.9012, -0.3709, -0.2241], 31 | [0.4330, 0.7500, 0.5000]]) 32 | dynamo_meta = get_conversion_metadata('dynamo') 33 | 34 | result_matrix = euler2matrix(dynamo_eulers, 35 | axes=dynamo_meta.axes, 36 | intrinsic=dynamo_meta.intrinsic, 37 | right_handed_rotation=dynamo_meta.right_handed_rotation) 38 | 39 | assert_array_almost_equal(dynamo_matrix, result_matrix, decimal=4) 40 | 41 | 42 | def test_euler2matrix_relion(): 43 | relion_eulers = [137.7000, 1.1777, 42.7270] 44 | relion_matrix = np.array([[-0.99985746, 0.00734648, -0.01520186], 45 | [-0.00755692, -0.99987577, 0.01383262], 46 | [-0.01509835, 0.01394553, 0.99978876]]) 47 | relion_meta = get_conversion_metadata('relion') 48 | 49 | result_matrix = euler2matrix(relion_eulers, 50 | axes=relion_meta.axes, 51 | intrinsic=relion_meta.intrinsic, 52 | right_handed_rotation=relion_meta.right_handed_rotation) 53 | 54 | assert_array_almost_equal(relion_matrix, result_matrix, decimal=4) 55 | -------------------------------------------------------------------------------- /test/test_matrix2euler.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_array_almost_equal 3 | 4 | from eulerangles import matrix2euler 5 | from eulerangles.math.constants import valid_axes 6 | from eulerangles.utils import get_conversion_metadata 7 | 8 | test_matrix_single = np.array([[-0.99985746, 0.00734648, -0.01520186], 9 | [-0.00755692, -0.99987577, 0.01383262], 10 | [-0.01509835, 0.01394553, 0.99978876]]) 11 | test_matrix_multiple = np.tile(test_matrix_single, (5, 1)) 12 | 13 | 14 | def test_matrix2euler_all_combinations(): 15 | for matrix in (test_matrix_single, test_matrix_multiple): 16 | for axes in valid_axes: 17 | for intrinsic in (True, False): 18 | for positive_ccw in (True, False): 19 | result = matrix2euler(matrix, 20 | axes=axes, 21 | intrinsic=True, 22 | right_handed_rotation=True) 23 | assert result.shape[-1] == 3 24 | 25 | 26 | def test_matrix2euler_dynamo(): 27 | dynamo_matrix = np.array([[-0.0173, -0.5477, 0.8365], 28 | [0.9012, -0.3709, -0.2241], 29 | [0.4330, 0.7500, 0.5000]]) 30 | dynamo_eulers = np.array([30, 60, 75]) 31 | dynamo_meta = get_conversion_metadata('dynamo') 32 | 33 | result_eulers = matrix2euler(dynamo_matrix, 34 | axes=dynamo_meta.axes, 35 | intrinsic=dynamo_meta.intrinsic, 36 | right_handed_rotation=dynamo_meta.right_handed_rotation) 37 | 38 | assert_array_almost_equal(dynamo_eulers, result_eulers, decimal=2) 39 | 40 | 41 | def test_matrix2euler_relion(): 42 | relion_matrix = np.array([[-0.99985746, 0.00734648, -0.01520186], 43 | [-0.00755692, -0.99987577, 0.01383262], 44 | [-0.01509835, 0.01394553, 0.99978876]]) 45 | relion_eulers = [137.7000, 1.1777, 42.7270] 46 | relion_meta = get_conversion_metadata('relion') 47 | 48 | result_eulers = matrix2euler(relion_matrix, 49 | axes=relion_meta.axes, 50 | intrinsic=relion_meta.intrinsic, 51 | right_handed_rotation=relion_meta.right_handed_rotation) 52 | 53 | assert_array_almost_equal(relion_eulers, result_eulers, decimal=4) 54 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | import recommonmark 7 | from recommonmark.parser import CommonMarkParser 8 | 9 | source_parsers = { 10 | '.md': CommonMarkParser 11 | } 12 | 13 | 14 | # -- Path setup -------------------------------------------------------------- 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 | conf_path = os.path.join( 23 | os.path.dirname(__file__), 24 | '../..') 25 | sys.path.insert(0, conf_path) 26 | sys.setrecursionlimit(1500) 27 | 28 | 29 | # -- Project information ----------------------------------------------------- 30 | 31 | project = 'eulerangles' 32 | copyright = '2021, Alister Burt' 33 | author = 'Alister Burt' 34 | 35 | # The full version, including alpha/beta/rc tags 36 | from eulerangles import __version__ 37 | release = __version__ 38 | 39 | 40 | # -- General configuration --------------------------------------------------- 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | 'sphinx.ext.autodoc', 47 | 'sphinx.ext.coverage', 48 | 'recommonmark', 49 | 'numpydoc', 50 | ] 51 | 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | templates_path = ['_templates'] 55 | 56 | # List of patterns, relative to source directory, that match files and 57 | # directories to ignore when looking for source files. 58 | # This pattern also affects html_static_path and html_extra_path. 59 | exclude_patterns = [] 60 | 61 | 62 | # -- Options for HTML output ------------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | # 67 | html_theme = 'alabaster' 68 | 69 | # Add any paths that contain custom static files (such as style sheets) here, 70 | # relative to this directory. They are copied after the builtin static files, 71 | # so a file named "default.css" will overwrite the builtin "default.css". 72 | html_static_path = ['_static'] 73 | -------------------------------------------------------------------------------- /eulerangles/math/eulers_to_eulers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .eulers_to_rotation_matrix import euler2matrix 4 | from .rotation_matrix_to_eulers import matrix2euler 5 | from .rotation_matrices.utils import invert_rotation_matrices 6 | 7 | 8 | def euler2euler(euler_angles: np.ndarray, 9 | source_axes: str, 10 | source_right_handed_rotation: bool, 11 | source_intrinsic: bool, 12 | target_axes: str, 13 | target_right_handed_rotation: bool, 14 | target_intrinsic: bool, 15 | invert_matrix: bool): 16 | """ 17 | Convert a set of Euler angles defined one way into a set of Euler angles defined another way. 18 | 19 | Parameters 20 | ---------- 21 | euler_angles : (n, 3) or (3,) array 22 | Euler angles to convert 23 | source_axes : str 24 | valid sequence of three non-sequential axes from 'x', 'y' and 'z' 25 | source_right_handed_rotation : bool 26 | True - Euler angles are interpreted as right handed rotations 27 | False - Euler angles are interpreted as left handed rotations 28 | source_intrinsic : bool 29 | True - Euler angles are interpreted as intrinsic rotations 30 | False - Euler angles are interpreted as extrinsic rotations 31 | target_axes : str 32 | valid sequence of three non-sequential axes from 'x', 'y' and 'z' 33 | target_right_handed_rotation : bool 34 | True - Euler angles are interpreted as right handed rotations 35 | False - Euler angles are interpreted as left handed rotations 36 | target_intrinsic : bool 37 | True - Euler angles are interpreted as intrinsic rotations 38 | False - Euler angles are interpreted as extrinsic rotations 39 | invert_matrix : bool 40 | True - rotation matrices will be inverted prior to deriving new Euler angles 41 | False - rotation matrices will not be inverted prior to deriving new Euler angles 42 | 43 | Returns 44 | ------- 45 | euler_angles : (n, 3) or (3,) array 46 | Euler angles generated from input Euler angles 47 | """ 48 | # Calculate rotation matrices from euler angles 49 | rotation_matrices = euler2matrix(euler_angles, 50 | source_axes, 51 | source_intrinsic, 52 | source_right_handed_rotation) 53 | 54 | # Invert matrices if one set of euler angles describe the inverse rotations of the desired 55 | # result 56 | if invert_matrix: 57 | rotation_matrices = invert_rotation_matrices(rotation_matrices) 58 | 59 | # Calculate euler angles in the target convention 60 | euler_angles = matrix2euler(rotation_matrices, 61 | target_axes, 62 | target_intrinsic, 63 | target_right_handed_rotation) 64 | return euler_angles.squeeze() 65 | 66 | 67 | -------------------------------------------------------------------------------- /eulerangles/math/rotation_matrices/angle_to_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def theta2rotx(theta: np.ndarray) -> np.ndarray: 5 | """ 6 | Rx = [[1, 0, 0], 7 | [0, c(t), -s(t)], 8 | [0, s(t), c(t)]] 9 | :param theta: angle(s) in degrees, positive is counterclockwise 10 | :return: rotation_matrices 11 | """ 12 | theta = np.deg2rad(np.asarray(theta).reshape(-1)) 13 | rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=float) 14 | cos_theta = np.cos(theta) 15 | sin_theta = np.sin(theta) 16 | rotation_matrices[:, 0, 0] = 1 17 | rotation_matrices[:, (1, 2), (1, 2)] = cos_theta[:, np.newaxis] 18 | rotation_matrices[:, 1, 2] = -sin_theta 19 | rotation_matrices[:, 2, 1] = sin_theta 20 | return rotation_matrices 21 | 22 | 23 | def theta2roty(theta: np.ndarray) -> np.ndarray: 24 | """ 25 | Ry = [[c(t), 0, s(t)], 26 | [0, 1, 0], 27 | [-s(t), 0, c(t)]] 28 | :param theta: angle(s) in degrees, positive is counterclockwise 29 | :return: rotation_matrices 30 | """ 31 | theta = np.deg2rad(np.asarray(theta).reshape(-1)) 32 | rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=float) 33 | cos_theta = np.cos(theta) 34 | sin_theta = np.sin(theta) 35 | rotation_matrices[:, 1, 1] = 1 36 | rotation_matrices[:, (0, 2), (0, 2)] = cos_theta[:, np.newaxis] 37 | rotation_matrices[:, 0, 2] = sin_theta 38 | rotation_matrices[:, 2, 0] = -sin_theta 39 | return rotation_matrices 40 | 41 | 42 | def theta2rotz(theta: np.ndarray) -> np.ndarray: 43 | """ 44 | Rz = [[c(t), -s(t), 0], 45 | [s(t), c(t), 0], 46 | [0, 0, 1]] 47 | :param theta: angle(s) in degrees, positive is counterclockwise 48 | :return: rotation_matrices 49 | """ 50 | theta = np.deg2rad(np.asarray(theta).reshape(-1)) 51 | rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=float) 52 | cos_theta = np.cos(theta) 53 | sin_theta = np.sin(theta) 54 | rotation_matrices[:, 2, 2] = 1 55 | rotation_matrices[:, (0, 1), (0, 1)] = cos_theta[:, np.newaxis] 56 | rotation_matrices[:, 0, 1] = -sin_theta 57 | rotation_matrices[:, 1, 0] = sin_theta 58 | return rotation_matrices 59 | 60 | 61 | def theta2rotm(theta: np.ndarray, axis: str): 62 | """ 63 | Convert values for theta into rotation matrices around a given axis 'x', 'y' or 'z' 64 | :param theta: angle(s) in degrees, positive is counterclockwise 65 | :param axis: 'x', 'y' or 'z' 66 | :return: rotation_matrices 67 | """ 68 | axis = axis.strip().lower() 69 | if axis not in ('x', 'y', 'z'): 70 | raise ValueError(f"Axis must be one of 'x', 'y' or 'z''") 71 | elif axis == 'x': 72 | rotation_matrices = theta2rotx(theta) 73 | elif axis == 'y': 74 | rotation_matrices = theta2roty(theta) 75 | elif axis == 'z': 76 | rotation_matrices = theta2rotz(theta) 77 | if rotation_matrices.shape[0] == 1: 78 | rotation_matrices = rotation_matrices.reshape((3, 3)) 79 | return rotation_matrices 80 | -------------------------------------------------------------------------------- /eulerangles/math/rotation_matrices/rotation_matrix_composition.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Sequence 3 | 4 | 5 | def compose_matrices_instrinsic(elemental_rotations: Sequence[np.ndarray]): 6 | """ 7 | Compose three sets of elemental rotation matrices intrinsically. 8 | 9 | Intrinsic rotations correspond to sequential rotations of a coordinate system which is 10 | mobile, moving with the rigid body after each rotation. 11 | 12 | axes = ax1, ax2, ax3 13 | angles = a, b, c 14 | R = R1(a) * R2(b) * R3(c) 15 | 16 | Parameters 17 | ---------- 18 | elemental_rotations : sequence of three (n, 3, 3) arrays 19 | arrays contain elemental rotation matrices to be composed 20 | 21 | Returns 22 | ------- 23 | rotation_matrices : (n, 3, 3) array 24 | result of intrinsic composition of the elemental matrices 25 | 26 | """ 27 | 28 | rotation_matrices = elemental_rotations[0] @ elemental_rotations[1] @ elemental_rotations[2] 29 | return rotation_matrices 30 | 31 | 32 | def compose_matrices_extrinsic(elemental_rotations: Sequence[np.ndarray]): 33 | """ 34 | Compose three sets of elemental rotation matrices extrinsically. 35 | 36 | Intrinsic rotations correspond to sequential rotations within a coordinate system which is 37 | fixed relative to a rotating rigid body. 38 | 39 | Extrinsic case, 40 | axes = ax1, ax2, ax3 41 | angles = a, b, c 42 | R = R3(c) * R2(b) * R1(a) 43 | 44 | Parameters 45 | ---------- 46 | elemental_rotations : sequence of three (n, 3, 3) arrays 47 | arrays contain elemental rotation matrices to be composed 48 | 49 | Returns 50 | ------- 51 | rotation_matrices : (n, 3, 3) array 52 | result of intrinsic composition of the elemental matrices 53 | 54 | 55 | """ 56 | rotation_matrices = elemental_rotations[2] @ elemental_rotations[1] @ elemental_rotations[0] 57 | return rotation_matrices 58 | 59 | 60 | def compose_rotation_matrices(elemental_rotations: Sequence[np.ndarray], mode: str): 61 | """ 62 | Compose three sets of elemental rotation matrices either intrinsically or extrinsically 63 | 64 | Parameters 65 | ---------- 66 | elemental_rotations : sequence of three (n, 3, 3) arrays 67 | arrays contain elemental rotation matrices to be composed 68 | mode : str 69 | either 'intrinsic' or 'extrinsic' depending upon desired mode of composition 70 | 71 | Returns 72 | ------- 73 | rotation_matrices : (n, 3, 3) array 74 | result of intrinsic composition of the elemental matrices 75 | """ 76 | if mode == 'intrinsic': 77 | rotation_matrices = compose_matrices_instrinsic(elemental_rotations) 78 | elif mode == 'extrinsic': 79 | rotation_matrices = compose_matrices_extrinsic(elemental_rotations) 80 | else: 81 | raise ValueError("mode must be 'intrinsic' or 'extrinsic'") 82 | return rotation_matrices 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eulerangles 2 | [![Build Status](https://travis-ci.com/alisterburt/eulerangles.svg?branch=master)](https://travis-ci.com/alisterburt/eulerangles) 3 | [![Documentation Status](https://readthedocs.org/projects/eulerangles/badge/?version=latest)](https://eulerangles.readthedocs.io/en/latest/?badge=latest) 4 | [![PyPI version](https://badge.fury.io/py/eulerangles.svg)](https://pypi.org/project/eulerangles/) 5 | [![PyPI pyversions](https://img.shields.io/pypi/pyversions/eulerangles.svg)](https://pypi.python.org/pypi/eulerangles/) 6 | 7 | [Euler angles](https://en.wikipedia.org/wiki/Euler_angles) are often used to represent rigid body rotations in 3D. 8 | 9 | These transformations can be defined in many different ways. The world of transformations is filled with 10 | [ambiguities](https://rock-learning.github.io/pytransform3d/transformation_ambiguities.html) which can make it harder 11 | than necessary to interface softwares which define their transformations differently. 12 | 13 | `eulerangles` is designed to simplify the handling of large sets of 14 | Euler angles in Python. 15 | 16 | ## Features 17 | - convert Euler angles into rotation matrices 18 | - convert rotation matrices into Euler angles 19 | - convert between differently defined Euler angles 20 | - simple API 21 | - vectorised implementation 22 | 23 | ## Documentation 24 | Complete documentation is provided on [readthedocs.io](https://eulerangles.readthedocs.io/en/latest/). 25 | - [Quick Start](https://eulerangles.readthedocs.io/en/latest/usage/quick_start.html) 26 | - [Installation](https://eulerangles.readthedocs.io/en/latest/usage/installation.html) 27 | - [API Reference](https://eulerangles.readthedocs.io/en/latest/api.html) 28 | 29 | ## Installation 30 | If you're already at ease with package management in Python, go ahead and 31 | ``` 32 | pip install eulerangles 33 | ``` 34 | 35 | Otherwise, please see the 36 | [installation](https://eulerangles.readthedocs.io/en/latest/usage/installation.html) page. 37 | 38 | ## Overview 39 | To keep usage simple, the package provides only five functions 40 | - `euler2matrix` 41 | - `matrix2euler` 42 | - `euler2euler` 43 | - `convert_eulers` 44 | - `invert_rotation_matrices` 45 | 46 | `euler2matrix` converts sets of Euler angles into rotation matrices. 47 | 48 | `matrix2euler` converts sets of rotation matrices into Euler angles. 49 | 50 | `euler2euler` converts between sets of Euler angles defined in different ways. 51 | 52 | `convert_eulers` provides a simpler interface to `euler2euler`. 53 | 54 | `invert_rotation_matrices` inverts sets of rotation matrices, yielding the inverse transformation. 55 | 56 | ## Examples 57 | ### `euler2matrix` 58 | 59 | ```python 60 | import numpy as np 61 | from eulerangles import euler2matrix 62 | 63 | eulers = np.array([30, 60, 75]) 64 | rotation_matrix = euler2matrix(eulers, 65 | axes='zyz', 66 | intrinsic=True, 67 | right_handed_rotation=True) 68 | ``` 69 | ```python 70 | >>> rotation_matrix 71 | array([[-0.37089098, -0.54766767, 0.75 ], 72 | [ 0.90122107, -0.01733759, 0.4330127 ], 73 | [-0.22414387, 0.8365163 , 0.5 ]]) 74 | ``` 75 | 76 | multiple sets of Euler angles can be passed as an (n, 3) array-like object, 77 | in which case an (n, 3, 3) array of rotation matrices will be returned. 78 | 79 | ### `matrix2euler` 80 | ```python 81 | import numpy as np 82 | from eulerangles import matrix2euler 83 | 84 | rotation_matrix = np.array([[-0.37089098, -0.54766767, 0.75 ], 85 | [ 0.90122107, -0.01733759, 0.4330127 ], 86 | [-0.22414387, 0.8365163 , 0.5 ]]) 87 | eulers = matrix2euler(rotation_matrix, 88 | axes='zyz', 89 | intrinsic=True, 90 | right_handed_rotation=True) 91 | ``` 92 | 93 | ```python 94 | >>> eulers 95 | array([29.99999989, 60. , 74.99999981]) 96 | ``` 97 | 98 | multiple rotation matrices can be passed as an (n, 3, 3) array-like object, 99 | in which case an (n, 3) array of Euler angles will be returned. 100 | 101 | ### `euler2euler` 102 | `euler2euler` is a verbose function for converting between sets of Euler angles defined differently. 103 | - `source_` parameters relate to the input Euler angles 104 | - `target_` parameters relate to the output Euler angles 105 | - `invert_matrix` inverts the rotation matrix or matrices before generating output Euler angles 106 | 107 | `invert_matrix` is useful when one set of Euler angles describe an active transformation, 108 | the other a passive transformation. 109 | 110 | ```python 111 | import numpy as np 112 | from eulerangles import euler2euler 113 | 114 | input_eulers = np.array([-47.2730, 1.1777, -132.3000]) 115 | output_eulers = euler2euler(input_eulers, 116 | source_axes='zxz', 117 | source_intrinsic=True, 118 | source_right_handed_rotation=True, 119 | target_axes='zyz', 120 | target_intrinsic=False, 121 | target_right_handed_rotation=False, 122 | invert_matrix=True) 123 | ``` 124 | ```python 125 | >>> output_eulers 126 | array([ 42.727 , -1.1777, 137.7 ]) 127 | ``` 128 | 129 | multiple sets of Euler angles can be passed as an (n, 3) array-like object, 130 | in which case an (n, 3) array of Euler angles will be returned. 131 | 132 | ### `convert_eulers` 133 | `convert_eulers` provides a simpler interface to the verbose `euler2euler` function. 134 | 135 | Necessary metadata for conversion between input Euler angles and output Euler angles 136 | are stored in a `ConversionMeta` objects, defined as follows 137 | ```python 138 | import numpy as np 139 | from eulerangles import ConversionMeta, convert_eulers 140 | 141 | source_metadata = ConversionMeta(name='input', 142 | axes='zyz', 143 | intrinsic=True, 144 | right_handed_rotation=True, 145 | active=True) 146 | 147 | target_metadata = ConversionMeta(name='output', 148 | axes='zxz', 149 | intrinsic=False, 150 | right_handed_rotation=False, 151 | active=False) 152 | ``` 153 | 154 | these objects are used for conversion in `convert_eulers` 155 | 156 | ```python 157 | input_eulers = np.array([10, 20, 30]) 158 | output_eulers = convert_eulers(input_eulers, 159 | source_meta=source_metadata, 160 | target_meta=target_metadata) 161 | ``` 162 | ```python 163 | >>> output_eulers 164 | array([-80., -20., 120.]) 165 | ``` 166 | 167 | For a select few software packages for 3D reconstruction from transmission electron microscopy images, 168 | `source_meta` and `target_meta` can be passed as a string corresponding to the name of the software package. 169 | 170 | ### `invert_rotation_matrices` 171 | `invert_rotation_matrices` inverts rotation matrices such that they represent 172 | the inverse transform of the input rotation matrix. 173 | 174 | Rotation matrices are orthogonal, therefore their inverse is simply the transpose. 175 | 176 | ```python 177 | import numpy as np 178 | from eulerangles import invert_rotation_matrices 179 | rotation_matrix = np.array([[-0.37089098, -0.54766767, 0.75 ], 180 | [ 0.90122107, -0.01733759, 0.4330127 ], 181 | [-0.22414387, 0.8365163 , 0.5 ]]) 182 | 183 | inverse_matrix = invert_rotation_matrices(rotation_matrix) 184 | ``` 185 | 186 | ```python 187 | >>> inverse_matrix 188 | array([[-0.37089098, 0.90122107, -0.22414387], 189 | [-0.54766767, -0.01733759, 0.8365163 ], 190 | [ 0.75 , 0.4330127 , 0.5 ]]) 191 | ``` 192 | 193 | This function works equally well on multiple rotation matrices passed as an (n, 3, 3) array. 194 | -------------------------------------------------------------------------------- /docs/source/usage/quick_start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | [Euler angles](https://en.wikipedia.org/wiki/Euler_angles) are often used to represent rigid body rotations in 3D. 3 | 4 | These transformations can be defined in many different ways. 5 | The world of transformations is filled with 6 | [ambiguities](https://rock-learning.github.io/pytransform3d/transformation_ambiguities.html) which can make it harder 7 | than necessary to interface softwares which define their transformations differently. 8 | 9 | `eulerangles` is designed to simplify the handling of large sets of 10 | Euler angles in Python. 11 | 12 | ## Installation 13 | If you're already at ease with package management in Python, go ahead and 14 | ``` 15 | pip install eulerangles 16 | ``` 17 | 18 | Otherwise, please see the [installation](installation.md) page. 19 | 20 | ## Overview 21 | To keep usage simple, the package provides only five functions 22 | - `euler2matrix` 23 | - `matrix2euler` 24 | - `euler2euler` 25 | - `convert_eulers` 26 | - `invert_rotation_matrices` 27 | 28 | `euler2matrix` converts sets of Euler angles into rotation matrices. 29 | 30 | `matrix2euler` converts sets of rotation matrices into Euler angles. 31 | 32 | `euler2euler` converts between sets of Euler angles defined in different ways. 33 | 34 | `convert_eulers` provides a simpler interface to `euler2euler`. 35 | 36 | `invert_rotation_matrices` inverts sets of rotation matrices, yielding the inverse transformation. 37 | 38 | ## General patterns 39 | Converting a set of Euler angles into a rotation matrix or vice-versa requires that we know 40 | 1. about which [axes](https://en.wikipedia.org/wiki/Euler_angles#Chained_rotations_equivalence) rotations occur 41 | 2. whether rotations are [intrinsic](https://en.wikipedia.org/wiki/Euler_angles#Conventions_by_intrinsic_rotations) or 42 | [extrinsic](https://en.wikipedia.org/wiki/Euler_angles#Conventions_by_extrinsic_rotations) 43 | 3. the [rotation handedness](https://en.wikipedia.org/wiki/Right-hand_rule#A_rotating_body) 44 | 45 | In this package 46 | - all angles are given in degrees 47 | - a right handed coordinate system is assumed 48 | - rotation matrices premultiply column vectors to produce transformed column vectors 49 | - sets of three axes are represented by a length 3 string containing only `'x'`, `'y'` or `'z'` 50 | e.g. `'zxz'`, `'xyz'`, `'zyz'` 51 | - instrinsic and extrinsic rotations are encoded by the `intrinsic` keyword argument in functions 52 | - `True` for intrinsic rotations 53 | - `False` for extrinsic rotations 54 | - rotation handedness is encoded by the `right_handed_rotation` keyword argument in functions 55 | - `True` for right handed rotations 56 | - `False` for left handed rotations 57 | 58 | - `ConversionMeta` objects package up this information for use in the `convert_eulers` function 59 | - Converting between active and passive transformations is handled by `invert_rotation_matrices` internally 60 | 61 | ## Examples 62 | ### `euler2matrix` 63 | 64 | ```python 65 | import numpy as np 66 | from eulerangles import euler2matrix 67 | 68 | eulers = np.array([30, 60, 75]) 69 | rotation_matrix = euler2matrix(eulers, 70 | axes='zyz', 71 | intrinsic=True, 72 | right_handed_rotation=True) 73 | ``` 74 | ```python 75 | >>> rotation_matrix 76 | array([[-0.37089098, -0.54766767, 0.75 ], 77 | [ 0.90122107, -0.01733759, 0.4330127 ], 78 | [-0.22414387, 0.8365163 , 0.5 ]]) 79 | ``` 80 | 81 | multiple sets of Euler angles can be passed as an (n, 3) array-like object, 82 | in which case an (n, 3, 3) array of rotation matrices will be returned. 83 | 84 | ### `matrix2euler` 85 | ```python 86 | import numpy as np 87 | from eulerangles import matrix2euler 88 | 89 | rotation_matrix = np.array([[-0.37089098, -0.54766767, 0.75 ], 90 | [ 0.90122107, -0.01733759, 0.4330127 ], 91 | [-0.22414387, 0.8365163 , 0.5 ]]) 92 | eulers = matrix2euler(rotation_matrix, 93 | axes='zyz', 94 | intrinsic=True, 95 | right_handed_rotation=True) 96 | ``` 97 | 98 | ```python 99 | >>> eulers 100 | array([29.99999989, 60. , 74.99999981]) 101 | ``` 102 | 103 | multiple rotation matrices can be passed as an (n, 3, 3) array-like object, 104 | in which case an (n, 3) array of Euler angles will be returned. 105 | 106 | ### `euler2euler` 107 | `euler2euler` is a verbose function for converting between sets of Euler angles defined differently. 108 | - `source_` parameters relate to the input Euler angles 109 | - `target_` parameters relate to the output Euler angles 110 | - `invert_matrix` inverts the rotation matrix or matrices before generating output Euler angles 111 | 112 | `invert_matrix` is useful when one set of Euler angles describe an active transformation, 113 | the other a passive transformation. 114 | 115 | ```python 116 | import numpy as np 117 | from eulerangles import euler2euler 118 | 119 | input_eulers = np.array([-47.2730, 1.1777, -132.3000]) 120 | output_eulers = euler2euler(input_eulers, 121 | source_axes='zxz', 122 | source_intrinsic=True, 123 | source_right_handed_rotation=True, 124 | target_axes='zyz', 125 | target_intrinsic=False, 126 | target_right_handed_rotation=False, 127 | invert_matrix=True) 128 | ``` 129 | ```python 130 | >>> output_eulers 131 | array([ 42.727 , -1.1777, 137.7 ]) 132 | ``` 133 | 134 | multiple sets of Euler angles can be passed as an (n, 3) array-like object, 135 | in which case an (n, 3) array of Euler angles will be returned. 136 | 137 | ### `convert_eulers` 138 | `convert_eulers` provides a simpler interface to the verbose `euler2euler` function. 139 | 140 | Necessary metadata for conversion between input Euler angles and output Euler angles 141 | are stored in a `ConversionMeta` objects, defined as follows 142 | ```python 143 | import numpy as np 144 | from eulerangles import ConversionMeta, convert_eulers 145 | 146 | source_metadata = ConversionMeta(name='input', 147 | axes='zyz', 148 | intrinsic=True, 149 | right_handed_rotation=True, 150 | active=True) 151 | 152 | target_metadata = ConversionMeta(name='output', 153 | axes='zxz', 154 | intrinsic=False, 155 | right_handed_rotation=False, 156 | active=False) 157 | ``` 158 | 159 | these objects are used for conversion in `convert_eulers` 160 | 161 | ```python 162 | input_eulers = np.array([10, 20, 30]) 163 | output_eulers = convert_eulers(input_eulers, 164 | source_meta=source_metadata, 165 | target_meta=target_metadata) 166 | ``` 167 | ```python 168 | >>> output_eulers 169 | array([-80., -20., 120.]) 170 | ``` 171 | 172 | For a select few software packages for 3D reconstruction from transmission electron microscopy images, 173 | `source_meta` and `target_meta` can be passed as a string corresponding to the name of the software package. 174 | 175 | ### `invert_rotation_matrices` 176 | `invert_rotation_matrices` inverts rotation matrices such that they represent 177 | the inverse transform of the input rotation matrix. 178 | 179 | Rotation matrices are orthogonal, therefore their inverse is simply the transpose. 180 | 181 | ```python 182 | import numpy as np 183 | from eulerangles import invert_rotation_matrices 184 | rotation_matrix = np.array([[-0.37089098, -0.54766767, 0.75 ], 185 | [ 0.90122107, -0.01733759, 0.4330127 ], 186 | [-0.22414387, 0.8365163 , 0.5 ]]) 187 | 188 | inverse_matrix = invert_rotation_matrices(rotation_matrix) 189 | ``` 190 | 191 | ```python 192 | >>> inverse_matrix 193 | array([[-0.37089098, 0.90122107, -0.22414387], 194 | [-0.54766767, -0.01733759, 0.8365163 ], 195 | [ 0.75 , 0.4330127 , 0.5 ]]) 196 | ``` 197 | 198 | This function works equally well on multiple rotation matrices passed as an (n, 3, 3) array. 199 | -------------------------------------------------------------------------------- /eulerangles/math/rotation_matrix_to_eulers.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .constants import valid_axes 4 | 5 | 6 | def matrix2xyx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 7 | """ 8 | Rx(k3) @ Ry(k2) @ Rx(k1) = [[c2, s1s2, c1s2], 9 | [s2s3, -s1c2s3+c1c3, -c1c2s3-s1c3], 10 | [-s2c3, s1c2c3+c1s3, c1c2c3-s1s3]] 11 | """ 12 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 13 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 14 | 15 | # Angle 2 can be taken directly from matrices 16 | angles_radians[:, 1] = np.arccos(rotation_matrices[:, 0, 0]) 17 | 18 | # Gimbal lock case (s2 = 0) 19 | tolerance = 1e-4 20 | 21 | # Find indices where this is the case 22 | gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance 23 | 24 | # Calculate angle 1 and set angle 3 = 0 for those indices 25 | r23 = rotation_matrices[gimbal_idx, 1, 2] 26 | r22 = rotation_matrices[gimbal_idx, 1, 1] 27 | angles_radians[gimbal_idx, 0] = np.arctan2(-r23, r22) 28 | angles_radians[gimbal_idx, 2] = 0 29 | 30 | # Normal case (s2 > 0) 31 | idx = np.invert(gimbal_idx) 32 | r12 = rotation_matrices[idx, 0, 1] 33 | r13 = rotation_matrices[idx, 0, 2] 34 | r21 = rotation_matrices[idx, 1, 0] 35 | r31 = rotation_matrices[idx, 2, 0] 36 | angles_radians[idx, 0] = np.arctan2(r12, r13) 37 | angles_radians[idx, 2] = np.arctan2(r21, -r31) 38 | 39 | # convert to degrees 40 | euler_angles = np.rad2deg(angles_radians) 41 | 42 | return euler_angles 43 | 44 | 45 | def matrix2yzy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 46 | """ 47 | Ry(k3) @ Rz(k2) @ Ry(k1) = [[c1c2c3-s1s3, -s2c3, s1c2c3+c1c3], 48 | [c1s2, c2, s1s2], 49 | [-c1c2s3, s2s3, -s1c2s3+c1c3]] 50 | """ 51 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 52 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 53 | 54 | # Angle 2 can be taken directly from matrices 55 | angles_radians[:, 1] = np.arccos(rotation_matrices[:, 1, 1]) 56 | 57 | # Gimbal lock case (s2 = 0) 58 | tolerance = 1e-4 59 | 60 | # Find indices where this is the case 61 | gimbal_idx = np.abs(rotation_matrices[:, 1, 0]) < tolerance 62 | 63 | # Calculate angle 1 and set angle 3 = 0 for those indices 64 | r31 = rotation_matrices[gimbal_idx, 2, 0] 65 | r33 = rotation_matrices[gimbal_idx, 2, 2] 66 | angles_radians[gimbal_idx, 0] = np.arctan2(-r31, r33) 67 | angles_radians[gimbal_idx, 2] = 0 68 | 69 | # Normal case (s2 > 0) 70 | idx = np.invert(gimbal_idx) 71 | r23 = rotation_matrices[idx, 1, 2] 72 | r21 = rotation_matrices[idx, 1, 0] 73 | r32 = rotation_matrices[idx, 2, 1] 74 | r12 = rotation_matrices[idx, 0, 1] 75 | angles_radians[idx, 0] = np.arctan2(r23, r21) 76 | angles_radians[idx, 2] = np.arctan2(r32, -r12) 77 | 78 | # convert to degrees 79 | euler_angles = np.rad2deg(angles_radians) 80 | return euler_angles 81 | 82 | 83 | def matrix2zxz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 84 | """ 85 | Rz(k3) @ Rx(k2) @ Rz(k1) = [[-s1c2s3+c1c3, -c1c2s3-s1c3, s2s3], 86 | [s1c2c3+s1s3, c1c2c3-s1s3, -s2c3], 87 | [s1s2, c1s2, c2]] 88 | """ 89 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 90 | angles = np.zeros((rotation_matrices.shape[0], 3)) 91 | 92 | # Angle 2 can be taken directly from matrices 93 | angles[:, 1] = np.arccos(rotation_matrices[:, 2, 2]) 94 | 95 | # Gimbal lock case (s2 = 0) 96 | tolerance = 1e-4 97 | 98 | # Find indices where this is the case 99 | gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance 100 | 101 | # Calculate angle 1 and set angle 3 = 0 for those indices 102 | r12 = rotation_matrices[gimbal_idx, 0, 1] 103 | r11 = rotation_matrices[gimbal_idx, 0, 0] 104 | angles[gimbal_idx, 0] = np.arctan2(-r12, r11) 105 | angles[gimbal_idx, 2] = 0 106 | 107 | # Normal case (s2 > 0) 108 | idx = np.invert(gimbal_idx) 109 | r31 = rotation_matrices[idx, 2, 0] 110 | r32 = rotation_matrices[idx, 2, 1] 111 | r13 = rotation_matrices[idx, 0, 2] 112 | r23 = rotation_matrices[idx, 1, 2] 113 | angles[idx, 0] = np.arctan2(r31, r32) 114 | angles[idx, 2] = np.arctan2(r13, -r23) 115 | 116 | # convert to degrees 117 | euler_angles = np.rad2deg(angles) 118 | 119 | return euler_angles 120 | 121 | 122 | def matrix2xzx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 123 | """ 124 | Rx(k3) @ Rz(k2) @ Rx(k1) = [[c2, -c1s2, s1s2], 125 | [s2c3, c1c2c3-s3, -s1c2c3-c1s3], 126 | [s2s3, c1c2s3+s1c3, -s1c2s3+c1c3]] 127 | """ 128 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 129 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 130 | 131 | # Angle 2 can be taken directly from matrices 132 | angles_radians[:, 1] = np.arccos(rotation_matrices[:, 0, 0]) 133 | 134 | # Gimbal lock case (s2 = 0) 135 | tolerance = 1e-4 136 | 137 | # Find indices where this is the case 138 | gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance 139 | 140 | # Calculate angle 1 and set angle 3 = 0 for those indices 141 | r32 = rotation_matrices[gimbal_idx, 2, 1] 142 | r33 = rotation_matrices[gimbal_idx, 2, 2] 143 | angles_radians[gimbal_idx, 0] = np.arctan2(r32, r33) 144 | angles_radians[gimbal_idx, 2] = 0 145 | 146 | # Normal case (s2 > 0) 147 | idx = np.invert(gimbal_idx) 148 | r13 = rotation_matrices[idx, 0, 2] 149 | r12 = rotation_matrices[idx, 0, 1] 150 | r31 = rotation_matrices[idx, 2, 0] 151 | r21 = rotation_matrices[idx, 1, 0] 152 | angles_radians[idx, 0] = np.arctan2(r13, -r12) 153 | angles_radians[idx, 2] = np.arctan2(r31, r21) 154 | 155 | # convert to degrees 156 | euler_angles = np.rad2deg(angles_radians) 157 | return euler_angles 158 | 159 | 160 | def matrix2yxy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 161 | """ 162 | Ry(k3) @ Rx(k2) @ Ry(k1) = [[-s1c2s3+c1c3, s2s3, c1c2s3+s1c3], 163 | [s1s2, c2, -c1s2], 164 | [-s1c2c3-c1s3, s2c3, c1c2c3-s1s3]] 165 | """ 166 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 167 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 168 | 169 | # Angle 2 can be taken directly from matrices 170 | angles_radians[:, 1] = np.arccos(rotation_matrices[:, 1, 1]) 171 | 172 | # Gimbal lock case (s2 = 0) 173 | tolerance = 1e-4 174 | 175 | # Find indices where this is the case 176 | gimbal_idx = np.abs(rotation_matrices[:, 0, 1]) < tolerance 177 | 178 | # Calculate angle 1 and set angle 3 = 0 for those indices 179 | r13 = rotation_matrices[gimbal_idx, 0, 2] 180 | r11 = rotation_matrices[gimbal_idx, 0, 0] 181 | angles_radians[gimbal_idx, 0] = np.arctan2(r13, r11) 182 | angles_radians[gimbal_idx, 2] = 0 183 | 184 | # Normal case (s2 > 0) 185 | idx = np.invert(gimbal_idx) 186 | r21 = rotation_matrices[idx, 1, 0] 187 | r23 = rotation_matrices[idx, 1, 2] 188 | r12 = rotation_matrices[idx, 0, 1] 189 | r32 = rotation_matrices[idx, 2, 1] 190 | angles_radians[idx, 0] = np.arctan2(r21, -r23) 191 | angles_radians[idx, 2] = np.arctan2(r12, r32) 192 | 193 | # convert to degrees 194 | euler_angles = np.rad2deg(angles_radians) 195 | 196 | return euler_angles 197 | 198 | 199 | def matrix2zyz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 200 | """ 201 | Rz(k3) @ Ry(k2) @ Rz(k1) = [[c1c2c3-s1s3, -s1c2c3-c1s3, s2c3], 202 | [c1c2s3+s1c3, -s1c2s3+c1c3, s2s3], 203 | [-c1s2, s1s2, c2]] 204 | """ 205 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 206 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 207 | 208 | # Angle 2 can be taken directly from matrices 209 | angles_radians[:, 1] = np.arccos(rotation_matrices[:, 2, 2]) 210 | 211 | # Gimbal lock case (s2 = 0) 212 | tolerance = 1e-4 213 | 214 | # Find indices where this is the case 215 | gimbal_idx = np.abs(rotation_matrices[:, 0, 2]) < tolerance 216 | 217 | # Calculate angle 1 and set angle 3 = 0 for those indices 218 | r21 = rotation_matrices[gimbal_idx, 1, 0] 219 | r22 = rotation_matrices[gimbal_idx, 1, 1] 220 | angles_radians[gimbal_idx, 0] = np.arctan2(r21, r22) 221 | angles_radians[gimbal_idx, 2] = 0 222 | 223 | # Normal case (s2 > 0) 224 | idx = np.invert(gimbal_idx) 225 | r32 = rotation_matrices[idx, 2, 1] 226 | r31 = rotation_matrices[idx, 2, 0] 227 | r23 = rotation_matrices[idx, 1, 2] 228 | r13 = rotation_matrices[idx, 0, 2] 229 | angles_radians[idx, 0] = np.arctan2(r32, -r31) 230 | angles_radians[idx, 2] = np.arctan2(r23, r13) 231 | 232 | # convert to degrees 233 | euler_angles = np.rad2deg(angles_radians) 234 | 235 | return euler_angles 236 | 237 | 238 | def matrix2xyz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 239 | """ 240 | Rz(k3) @ Ry(k2) @ Rx(k1) = [[c2c3, s1s2c3-c1s3, c1s2c3+s1s3], 241 | [c2s3, s1s2s3+c1c3, c1s2s3-s1c3], 242 | [-s2, s1c2, c1c2]] 243 | """ 244 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 245 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 246 | 247 | # Angle 2 can be taken directly from matrices 248 | angles_radians[:, 1] = -np.arcsin(rotation_matrices[:, 2, 0]) 249 | 250 | # Gimbal lock case (c2 = 0) 251 | tolerance = 1e-4 252 | 253 | # Find indices where this is the case 254 | gimbal_idx = np.abs(rotation_matrices[:, 0, 0]) < tolerance 255 | 256 | # Calculate angle 1 and set angle 3 = 0 for those indices 257 | r23 = rotation_matrices[gimbal_idx, 1, 2] 258 | r22 = rotation_matrices[gimbal_idx, 1, 1] 259 | angles_radians[gimbal_idx, 0] = np.arctan2(-r23, r22) 260 | angles_radians[gimbal_idx, 2] = 0 261 | 262 | # Normal case (s2 > 0) 263 | idx = np.invert(gimbal_idx) 264 | r32 = rotation_matrices[idx, 2, 1] 265 | r33 = rotation_matrices[idx, 2, 2] 266 | r21 = rotation_matrices[idx, 1, 0] 267 | r11 = rotation_matrices[idx, 0, 0] 268 | angles_radians[idx, 0] = np.arctan2(r32, r33) 269 | angles_radians[idx, 2] = np.arctan2(r21, r11) 270 | 271 | # convert to degrees 272 | euler_angles = np.rad2deg(angles_radians) 273 | 274 | return euler_angles 275 | 276 | 277 | def matrix2yzx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 278 | """ 279 | Rx(k3) @ Rz(k2) @ Ry(k1) = [[c1c2, -s2, s1c2], 280 | [c1s2c3+s1s3, c2c3, s1s2c3-c1s3], 281 | [c1s2s3-s1c3, c2s3, s1s2s3+c1c3]] 282 | """ 283 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 284 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 285 | 286 | # Angle 2 can be taken directly from matrices 287 | angles_radians[:, 1] = -np.arcsin(rotation_matrices[:, 0, 1]) 288 | 289 | # Gimbal lock case (c2 = 0) 290 | tolerance = 1e-4 291 | 292 | # Find indices where this is the case 293 | gimbal_idx = np.abs(rotation_matrices[:, 0, 0]) < tolerance 294 | 295 | # Calculate angle 1 and set angle 3 = 0 for those indices 296 | r31 = rotation_matrices[gimbal_idx, 2, 0] 297 | r33 = rotation_matrices[gimbal_idx, 2, 2] 298 | angles_radians[gimbal_idx, 0] = np.arctan2(-r31, r33) 299 | angles_radians[gimbal_idx, 2] = 0 300 | 301 | # Normal case (s2 > 0) 302 | idx = np.invert(gimbal_idx) 303 | r13 = rotation_matrices[idx, 0, 2] 304 | r11 = rotation_matrices[idx, 0, 0] 305 | r32 = rotation_matrices[idx, 2, 1] 306 | r22 = rotation_matrices[idx, 1, 1] 307 | angles_radians[idx, 0] = np.arctan2(r13, r11) 308 | angles_radians[idx, 2] = np.arctan2(r32, r22) 309 | 310 | # convert to degrees 311 | euler_angles = np.rad2deg(angles_radians) 312 | 313 | return euler_angles 314 | 315 | 316 | def matrix2zxy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 317 | """ 318 | Ry(k3) @ Rx(k2) @ Rz(k1) = [[s1s2s3+c1c3, c1s2s3-s1c3, c2s3], 319 | [s1c2, c1c2, -s2], 320 | [s1s2c3-c1s3, c1s2c3+s1s3, c2c3]] 321 | """ 322 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 323 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 324 | 325 | # Angle 2 can be taken directly from matrices 326 | angles_radians[:, 1] = -np.arcsin(rotation_matrices[:, 1, 2]) 327 | 328 | # Gimbal lock case (c2 = 0) 329 | tolerance = 1e-4 330 | 331 | # Find indices where this is the case 332 | gimbal_idx = np.abs(rotation_matrices[:, 1, 0]) < tolerance 333 | 334 | # Calculate angle 1 and set angle 3 = 0 for those indices 335 | r12 = rotation_matrices[gimbal_idx, 0, 1] 336 | r11 = rotation_matrices[gimbal_idx, 0, 0] 337 | angles_radians[gimbal_idx, 0] = np.arctan2(-r12, r11) 338 | angles_radians[gimbal_idx, 2] = 0 339 | 340 | # Normal case (s2 > 0) 341 | idx = np.invert(gimbal_idx) 342 | r21 = rotation_matrices[idx, 1, 0] 343 | r22 = rotation_matrices[idx, 1, 1] 344 | r13 = rotation_matrices[idx, 0, 2] 345 | r33 = rotation_matrices[idx, 2, 2] 346 | angles_radians[idx, 0] = np.arctan2(r21, r22) 347 | angles_radians[idx, 2] = np.arctan2(r13, r33) 348 | 349 | # convert to degrees 350 | euler_angles = np.rad2deg(angles_radians) 351 | 352 | return euler_angles 353 | 354 | 355 | def matrix2xzy_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 356 | """ 357 | Ry(k3) @ Rz(k2) @ Rx(k1) = [[c2c3, -c1s2c3+s1s3, s1s2c3+c1s3], 358 | [s2, c1c2, -s1c2], 359 | [-c2s3, c1s2s3+s1c3, -s1s2s3+c1c3]] 360 | """ 361 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 362 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 363 | 364 | # Angle 2 can be taken directly from matrices 365 | angles_radians[:, 1] = np.arcsin(rotation_matrices[:, 1, 0]) 366 | 367 | # Gimbal lock case (c2 = 0) 368 | tolerance = 1e-4 369 | 370 | # Find indices where this is the case 371 | gimbal_idx = np.abs(rotation_matrices[:, 0, 0]) < tolerance 372 | 373 | # Calculate angle 1 and set angle 3 = 0 for those indices 374 | r32 = rotation_matrices[gimbal_idx, 2, 1] 375 | r33 = rotation_matrices[gimbal_idx, 2, 2] 376 | angles_radians[gimbal_idx, 0] = np.arctan2(r32, r33) 377 | angles_radians[gimbal_idx, 2] = 0 378 | 379 | # Normal case (s2 > 0) 380 | idx = np.invert(gimbal_idx) 381 | r23 = rotation_matrices[idx, 1, 2] 382 | r22 = rotation_matrices[idx, 1, 1] 383 | r31 = rotation_matrices[idx, 2, 0] 384 | r11 = rotation_matrices[idx, 0, 0] 385 | angles_radians[idx, 0] = np.arctan2(-r23, r22) 386 | angles_radians[idx, 2] = np.arctan2(-r31, r11) 387 | 388 | # convert to degrees 389 | euler_angles = np.rad2deg(angles_radians) 390 | 391 | return euler_angles 392 | 393 | 394 | def matrix2yxz_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 395 | """ 396 | Rz(k3) @ Rx(k2) @ Ry(k1) = [[-s1s2s3+c1c3, -c2s3, c1s2s3+s1c3], 397 | [s1s2c3+c1s3, c2c3, -c1s2c3+s1s3], 398 | [-s1c2, s2, c1c2]] 399 | """ 400 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 401 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 402 | 403 | # Angle 2 can be taken directly from matrices 404 | angles_radians[:, 1] = np.arcsin(rotation_matrices[:, 2, 1]) 405 | 406 | # Gimbal lock case (c2 = 0) 407 | tolerance = 1e-4 408 | 409 | # Find indices where this is the case 410 | gimbal_idx = np.abs(rotation_matrices[:, 1, 1]) < tolerance 411 | 412 | # Calculate angle 1 and set angle 3 = 0 for those indices 413 | r13 = rotation_matrices[gimbal_idx, 0, 2] 414 | r11 = rotation_matrices[gimbal_idx, 0, 0] 415 | angles_radians[gimbal_idx, 0] = np.arctan2(r13, r11) 416 | angles_radians[gimbal_idx, 2] = 0 417 | 418 | # Normal case (s2 > 0) 419 | idx = np.invert(gimbal_idx) 420 | r31 = rotation_matrices[idx, 2, 0] 421 | r33 = rotation_matrices[idx, 2, 2] 422 | r12 = rotation_matrices[idx, 0, 1] 423 | r22 = rotation_matrices[idx, 1, 1] 424 | angles_radians[idx, 0] = np.arctan2(-r31, r33) 425 | angles_radians[idx, 2] = np.arctan2(-r12, r22) 426 | 427 | # convert to degrees 428 | euler_angles = np.rad2deg(angles_radians) 429 | 430 | return euler_angles 431 | 432 | 433 | def matrix2zyx_extrinsic(rotation_matrices: np.ndarray) -> np.ndarray: 434 | """ 435 | Rx(k3) @ Ry(k2) @ Rz(k1) = [[c1c2, -s1c2, s2], 436 | [c1s2s3+s1c3, -s1s2s3+c1c3, -c2s3], 437 | [-c1s2c3+s1s3, s1s2c3+c1s3, c2c3]] 438 | """ 439 | rotation_matrices = rotation_matrices.reshape((-1, 3, 3)) 440 | angles_radians = np.zeros((rotation_matrices.shape[0], 3)) 441 | 442 | # Angle 2 can be taken directly from matrices 443 | angles_radians[:, 1] = np.arcsin(rotation_matrices[:, 0, 2]) 444 | 445 | # Gimbal lock case (c2 = 0) 446 | tolerance = 1e-4 447 | 448 | # Find indices where this is the case 449 | gimbal_idx = np.abs(rotation_matrices[:, 1, 1]) < tolerance 450 | 451 | # Calculate angle 1 and set angle 3 = 0 for those indices 452 | r21 = rotation_matrices[gimbal_idx, 1, 0] 453 | r22 = rotation_matrices[gimbal_idx, 1, 1] 454 | angles_radians[gimbal_idx, 0] = np.arctan2(r21, r22) 455 | angles_radians[gimbal_idx, 2] = 0 456 | 457 | # Normal case (s2 > 0) 458 | idx = np.invert(gimbal_idx) 459 | r12 = rotation_matrices[idx, 0, 1] 460 | r11 = rotation_matrices[idx, 0, 0] 461 | r23 = rotation_matrices[idx, 1, 2] 462 | r33 = rotation_matrices[idx, 2, 2] 463 | angles_radians[idx, 0] = np.arctan2(-r12, r11) 464 | angles_radians[idx, 2] = np.arctan2(-r23, r33) 465 | 466 | # convert to degrees 467 | euler_angles = np.rad2deg(angles_radians) 468 | 469 | return euler_angles 470 | 471 | 472 | def matrix2euler_extrinsic(rotation_matrices: np.ndarray, axes: str): 473 | matrix2euler_function = extrinsic_matrix2euler_functions[axes] 474 | return matrix2euler_function(rotation_matrices) 475 | 476 | 477 | def matrix2euler_intrinsic(rotation_matrices: np.ndarray, axes: str): 478 | """ 479 | It can be shown that a set of intrinsic rotations about axes x then y then z through angles 480 | α, β, γ is equivalent to a set of extrinsic rotations about axes z then y then x 481 | by angles γ, β, α. 482 | """ 483 | extrinsic_axes = axes[::-1] 484 | extrinsic_eulers = matrix2euler_extrinsic(rotation_matrices, extrinsic_axes) 485 | intrinsic_eulers = extrinsic_eulers[:, ::-1] 486 | return intrinsic_eulers 487 | 488 | 489 | def matrix2euler_right_handed(rotation_matrices: np.ndarray, axes: str, intrinsic: bool): 490 | if intrinsic: 491 | return matrix2euler_intrinsic(rotation_matrices, axes) 492 | else: 493 | return matrix2euler_extrinsic(rotation_matrices, axes) 494 | 495 | 496 | def matrix2euler(rotation_matrices: np.ndarray, 497 | axes: str, 498 | intrinsic: bool, 499 | right_handed_rotation: bool, 500 | ) -> np.ndarray: 501 | """ 502 | Derive a set of euler angles from a set of rotation matrices. 503 | 504 | Parameters 505 | ---------- 506 | rotation_matrices : (n, 3, 3) or (3, 3) array of float 507 | rotation matrices from which euler angles are derived 508 | axes : str 509 | valid sequence of three non-sequential axes from 'x', 'y' and 'z' 510 | e.g. 'zyz', 'zxz', 'xyz' 511 | intrinsic : bool 512 | True - Euler angles are for intrinsic rotations 513 | False - Euler angles are for extrinsic rotations 514 | right_handed_rotation : bool 515 | True - Euler angles are for right handed rotations 516 | False - Euler angles are for left handed rotations 517 | 518 | Returns 519 | ------- 520 | euler_angles : (n, 3) or (3,) array 521 | Euler angles derived from rotation matrices 522 | """ 523 | # Sanitise and check input 524 | rotation_matrices = np.asarray(rotation_matrices).reshape((-1, 3, 3)) 525 | formatted_axes = axes.strip().lower() 526 | 527 | if formatted_axes not in valid_axes: 528 | raise ValueError(f'Axes {axes} are not a valid set of euler angle axes') 529 | 530 | axes = formatted_axes 531 | 532 | # Calculate euler angles for right handed rotations 533 | euler_angles = matrix2euler_right_handed(rotation_matrices, axes, intrinsic) 534 | 535 | # If you want left handed rotations, invert the angles 536 | if not right_handed_rotation: 537 | euler_angles *= -1 538 | 539 | return euler_angles.squeeze() 540 | 541 | 542 | extrinsic_matrix2euler_functions = { 543 | 'xyz': matrix2xyz_extrinsic, 544 | 'xyx': matrix2xyx_extrinsic, 545 | 'xzx': matrix2xzx_extrinsic, 546 | 'xzy': matrix2xzy_extrinsic, 547 | 'yxy': matrix2yxy_extrinsic, 548 | 'yxz': matrix2yxz_extrinsic, 549 | 'yzx': matrix2yzx_extrinsic, 550 | 'yzy': matrix2yzy_extrinsic, 551 | 'zxy': matrix2zxy_extrinsic, 552 | 'zxz': matrix2zxz_extrinsic, 553 | 'zyx': matrix2zyx_extrinsic, 554 | 'zyz': matrix2zyz_extrinsic, 555 | } 556 | --------------------------------------------------------------------------------