vr180-convert 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2024 34j
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.1
2
+ Name: vr180-convert
3
+ Version: 0.1.0
4
+ Summary: Simple VR180 image converter
5
+ Home-page: https://github.com/34j/vr180-convert
6
+ License: MIT
7
+ Author: 34j
8
+ Author-email: 34j.95a2p@simplelogin.com
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Dist: attrs (>=23.2.0,<24.0.0)
22
+ Requires-Dist: numpy-quaternion (>=2023.0.3,<2024.0.0)
23
+ Requires-Dist: opencv-python (>=4.9.0.80,<5.0.0.0)
24
+ Requires-Dist: rich (>=10)
25
+ Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
26
+ Requires-Dist: strenum (>=0.4.15,<0.5.0)
27
+ Requires-Dist: typer[all] (>=0.9.0,<0.10.0)
28
+ Project-URL: Bug Tracker, https://github.com/34j/vr180-convert/issues
29
+ Project-URL: Changelog, https://github.com/34j/vr180-convert/blob/main/CHANGELOG.md
30
+ Project-URL: Documentation, https://vr180-convert.readthedocs.io
31
+ Project-URL: Repository, https://github.com/34j/vr180-convert
32
+ Description-Content-Type: text/markdown
33
+
34
+ # VR180 image converter
35
+
36
+ <p align="center">
37
+ <a href="https://github.com/34j/vr180-convert/actions/workflows/ci.yml?query=branch%3Amain">
38
+ <img src="https://img.shields.io/github/actions/workflow/status/34j/vr180-convert/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
39
+ </a>
40
+ <a href="https://vr180-convert.readthedocs.io">
41
+ <img src="https://img.shields.io/readthedocs/vr180-convert.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
42
+ </a>
43
+ <a href="https://codecov.io/gh/34j/vr180-convert">
44
+ <img src="https://img.shields.io/codecov/c/github/34j/vr180-convert.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
45
+ </a>
46
+ </p>
47
+ <p align="center">
48
+ <a href="https://python-poetry.org/">
49
+ <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
50
+ </a>
51
+ <a href="https://github.com/ambv/black">
52
+ <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
53
+ </a>
54
+ <a href="https://github.com/pre-commit/pre-commit">
55
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
56
+ </a>
57
+ </p>
58
+ <p align="center">
59
+ <a href="https://pypi.org/project/vr180-convert/">
60
+ <img src="https://img.shields.io/pypi/v/vr180-convert.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
61
+ </a>
62
+ <img src="https://img.shields.io/pypi/pyversions/vr180-convert.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
63
+ <img src="https://img.shields.io/pypi/l/vr180-convert.svg?style=flat-square" alt="License">
64
+ </p>
65
+
66
+ ---
67
+
68
+ **Documentation**: <a href="https://vr180-convert.readthedocs.io" target="_blank">https://vr180-convert.readthedocs.io </a>
69
+
70
+ **Source Code**: <a href="https://github.com/34j/vr180-convert" target="_blank">https://github.com/34j/vr180-convert </a>
71
+
72
+ ---
73
+
74
+ Simple VR180 image converter on top of OpenCV and NumPy.
75
+
76
+ ## Installation
77
+
78
+ Install this via pip (or your favourite package manager):
79
+
80
+ ```shell
81
+ pipx install vr180-convert
82
+ ```
83
+
84
+ | Left | Right | Output |
85
+ | ------------------------------ | ------------------------------- | ---------------------------------------------------- |
86
+ | ![left](docs/_static/test.jpg) | ![right](docs/_static/test.jpg) | ![output](docs/_static/test.lr.PolynomialScaler.jpg) |
87
+
88
+ ## Usage
89
+
90
+ Simply run the following command to convert 2 fisheye images to a SBS equirectangular VR180 image:
91
+
92
+ ```shell
93
+ v1c lr left.jpg right.jpg
94
+ ```
95
+
96
+ You can also specify the conversion model by adding Python code directly to the `--transformer` option:
97
+
98
+ ```shell
99
+ v1c lr left.jpg right.jpg ---transformer "EquirectangularEncoder() * Euclidean3DRotator(from_rotation_vector([0, np.pi / 4, 0])) * FisheyeDecoder("equidistant")"
100
+ ```
101
+
102
+ Please refer to the [API documentation](https://vr180-convert.readthedocs.io/) for the available transformers and their parameters.
103
+ For `from_rotation_vector`, please refer to the [numpy-quaternion documentation](https://quaternion.readthedocs.io/en/latest/Package%20API%3A/quaternion/#from_rotation_vector).
104
+
105
+ The radius of the non-black area of the input image is assumed by counting black pixels by default, but it would be better to specify it manually to get stable results:
106
+
107
+ ```shell
108
+ v1c lr left.jpg right.jpg --radius 1000
109
+ v1c lr left.jpg right.jpg --radius max # min(width, height) / 2
110
+ ```
111
+
112
+ To convert a single image, use `v1c s` instead.
113
+
114
+ For more information, please refer to the help or API documentation:
115
+
116
+ ```shell
117
+ v1c --help
118
+ ```
119
+
120
+ ## Usage as a library
121
+
122
+ For more complex transformations, it is recommended to create your own `Transformer`.
123
+
124
+ Note that the transformation is applied in inverse order (new[(x, y)] = old[transform(x, y)], e.g. to decode [orthographic](https://en.wikipedia.org/wiki/Fisheye_lens#Mapping_function) fisheye images, `transform_polar` should be `arcsin(theta)`, not `sin(theta)`.)
125
+
126
+ ```python
127
+ from vr180_convert import PolarRollTransformer, apply_lr
128
+
129
+ class MyTransformer(PolarRollTransformer):
130
+ def transform_polar(
131
+ self, theta: NDArray, roll: NDArray, **kwargs: Any
132
+ ) -> tuple[NDArray, NDArray]:
133
+ return theta**0.98 + theta**1.01, roll
134
+
135
+ transformer = EquirectangularEncoder() * MyTransformer() * FisheyeDecoder("equidistant")
136
+ apply_lr(transformer, left_path="left.jpg", right_path="right.jpg", out_path="output.jpg")
137
+ ```
138
+
139
+ ## Contributors ✨
140
+
141
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
142
+
143
+ <!-- prettier-ignore-start -->
144
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
145
+ <!-- markdownlint-disable -->
146
+ <!-- markdownlint-enable -->
147
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
148
+ <!-- prettier-ignore-end -->
149
+
150
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
151
+
152
+ ## Credits
153
+
154
+ This package was created with
155
+ [Copier](https://copier.readthedocs.io/) and the
156
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
157
+ project template.
158
+
@@ -0,0 +1,124 @@
1
+ # VR180 image converter
2
+
3
+ <p align="center">
4
+ <a href="https://github.com/34j/vr180-convert/actions/workflows/ci.yml?query=branch%3Amain">
5
+ <img src="https://img.shields.io/github/actions/workflow/status/34j/vr180-convert/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
6
+ </a>
7
+ <a href="https://vr180-convert.readthedocs.io">
8
+ <img src="https://img.shields.io/readthedocs/vr180-convert.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
9
+ </a>
10
+ <a href="https://codecov.io/gh/34j/vr180-convert">
11
+ <img src="https://img.shields.io/codecov/c/github/34j/vr180-convert.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
12
+ </a>
13
+ </p>
14
+ <p align="center">
15
+ <a href="https://python-poetry.org/">
16
+ <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
17
+ </a>
18
+ <a href="https://github.com/ambv/black">
19
+ <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
20
+ </a>
21
+ <a href="https://github.com/pre-commit/pre-commit">
22
+ <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
23
+ </a>
24
+ </p>
25
+ <p align="center">
26
+ <a href="https://pypi.org/project/vr180-convert/">
27
+ <img src="https://img.shields.io/pypi/v/vr180-convert.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
28
+ </a>
29
+ <img src="https://img.shields.io/pypi/pyversions/vr180-convert.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
30
+ <img src="https://img.shields.io/pypi/l/vr180-convert.svg?style=flat-square" alt="License">
31
+ </p>
32
+
33
+ ---
34
+
35
+ **Documentation**: <a href="https://vr180-convert.readthedocs.io" target="_blank">https://vr180-convert.readthedocs.io </a>
36
+
37
+ **Source Code**: <a href="https://github.com/34j/vr180-convert" target="_blank">https://github.com/34j/vr180-convert </a>
38
+
39
+ ---
40
+
41
+ Simple VR180 image converter on top of OpenCV and NumPy.
42
+
43
+ ## Installation
44
+
45
+ Install this via pip (or your favourite package manager):
46
+
47
+ ```shell
48
+ pipx install vr180-convert
49
+ ```
50
+
51
+ | Left | Right | Output |
52
+ | ------------------------------ | ------------------------------- | ---------------------------------------------------- |
53
+ | ![left](docs/_static/test.jpg) | ![right](docs/_static/test.jpg) | ![output](docs/_static/test.lr.PolynomialScaler.jpg) |
54
+
55
+ ## Usage
56
+
57
+ Simply run the following command to convert 2 fisheye images to a SBS equirectangular VR180 image:
58
+
59
+ ```shell
60
+ v1c lr left.jpg right.jpg
61
+ ```
62
+
63
+ You can also specify the conversion model by adding Python code directly to the `--transformer` option:
64
+
65
+ ```shell
66
+ v1c lr left.jpg right.jpg ---transformer "EquirectangularEncoder() * Euclidean3DRotator(from_rotation_vector([0, np.pi / 4, 0])) * FisheyeDecoder("equidistant")"
67
+ ```
68
+
69
+ Please refer to the [API documentation](https://vr180-convert.readthedocs.io/) for the available transformers and their parameters.
70
+ For `from_rotation_vector`, please refer to the [numpy-quaternion documentation](https://quaternion.readthedocs.io/en/latest/Package%20API%3A/quaternion/#from_rotation_vector).
71
+
72
+ The radius of the non-black area of the input image is assumed by counting black pixels by default, but it would be better to specify it manually to get stable results:
73
+
74
+ ```shell
75
+ v1c lr left.jpg right.jpg --radius 1000
76
+ v1c lr left.jpg right.jpg --radius max # min(width, height) / 2
77
+ ```
78
+
79
+ To convert a single image, use `v1c s` instead.
80
+
81
+ For more information, please refer to the help or API documentation:
82
+
83
+ ```shell
84
+ v1c --help
85
+ ```
86
+
87
+ ## Usage as a library
88
+
89
+ For more complex transformations, it is recommended to create your own `Transformer`.
90
+
91
+ Note that the transformation is applied in inverse order (new[(x, y)] = old[transform(x, y)], e.g. to decode [orthographic](https://en.wikipedia.org/wiki/Fisheye_lens#Mapping_function) fisheye images, `transform_polar` should be `arcsin(theta)`, not `sin(theta)`.)
92
+
93
+ ```python
94
+ from vr180_convert import PolarRollTransformer, apply_lr
95
+
96
+ class MyTransformer(PolarRollTransformer):
97
+ def transform_polar(
98
+ self, theta: NDArray, roll: NDArray, **kwargs: Any
99
+ ) -> tuple[NDArray, NDArray]:
100
+ return theta**0.98 + theta**1.01, roll
101
+
102
+ transformer = EquirectangularEncoder() * MyTransformer() * FisheyeDecoder("equidistant")
103
+ apply_lr(transformer, left_path="left.jpg", right_path="right.jpg", out_path="output.jpg")
104
+ ```
105
+
106
+ ## Contributors ✨
107
+
108
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
109
+
110
+ <!-- prettier-ignore-start -->
111
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
112
+ <!-- markdownlint-disable -->
113
+ <!-- markdownlint-enable -->
114
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
115
+ <!-- prettier-ignore-end -->
116
+
117
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
118
+
119
+ ## Credits
120
+
121
+ This package was created with
122
+ [Copier](https://copier.readthedocs.io/) and the
123
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
124
+ project template.
@@ -0,0 +1,160 @@
1
+ [tool.poetry]
2
+ name = "vr180-convert"
3
+ version = "0.1.0"
4
+ description = "Simple VR180 image converter"
5
+ authors = ["34j <34j.95a2p@simplelogin.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://github.com/34j/vr180-convert"
9
+ documentation = "https://vr180-convert.readthedocs.io"
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Natural Language :: English",
14
+ "Operating System :: OS Independent",
15
+ "Topic :: Software Development :: Libraries",
16
+ ]
17
+ packages = [
18
+ { include = "vr180_convert", from = "src" },
19
+ ]
20
+
21
+ [tool.poetry.urls]
22
+ "Bug Tracker" = "https://github.com/34j/vr180-convert/issues"
23
+ "Changelog" = "https://github.com/34j/vr180-convert/blob/main/CHANGELOG.md"
24
+
25
+ [tool.poetry.scripts]
26
+ vr180-convert = "vr180_convert.cli:app"
27
+ v1c = "vr180_convert.cli:app"
28
+
29
+ [tool.poetry.dependencies]
30
+ python = "^3.9"
31
+ rich = ">=10"
32
+ typer = {extras = ["all"], version = "^0.9.0"}
33
+ opencv-python = "^4.9.0.80"
34
+ attrs = "^23.2.0"
35
+ scikit-learn = "^1.4.2"
36
+ numpy-quaternion = "^2023.0.3"
37
+ strenum = "^0.4.15"
38
+
39
+ [tool.poetry.group.dev.dependencies]
40
+ pytest = "^7.0"
41
+ pytest-cov = "^3.0"
42
+
43
+ [tool.poetry.group.docs]
44
+ optional = true
45
+
46
+ [tool.poetry.group.docs.dependencies]
47
+ myst-parser = { version = ">=0.16", python = ">=3.11"}
48
+ sphinx = { version = ">=4.0", python = ">=3.11"}
49
+ furo = { version = ">=2023.5.20", python = ">=3.11"}
50
+ sphinx-autobuild = { version = ">=2024.0.0", python = ">=3.11"}
51
+
52
+ [tool.semantic_release]
53
+ version_toml = ["pyproject.toml:tool.poetry.version"]
54
+ version_variables = [
55
+ "src/vr180_convert/__init__.py:__version__",
56
+ "docs/conf.py:release",
57
+ ]
58
+ build_command = "pip install poetry && poetry build"
59
+
60
+ [tool.semantic_release.changelog]
61
+ exclude_commit_patterns = [
62
+ "chore*",
63
+ "ci*",
64
+ ]
65
+
66
+ [tool.semantic_release.changelog.environment]
67
+ keep_trailing_newline = true
68
+
69
+ [tool.semantic_release.branches.main]
70
+ match = "main"
71
+
72
+ [tool.semantic_release.branches.noop]
73
+ match = "(?!main$)"
74
+ prerelease = true
75
+
76
+ [tool.pytest.ini_options]
77
+ addopts = "-v -Wdefault --cov=vr180_convert --cov-report=term-missing:skip-covered"
78
+ pythonpath = ["src"]
79
+
80
+ [tool.coverage.run]
81
+ branch = true
82
+
83
+ [tool.coverage.report]
84
+ exclude_lines = [
85
+ "pragma: no cover",
86
+ "@overload",
87
+ "if TYPE_CHECKING",
88
+ "raise NotImplementedError",
89
+ 'if __name__ == "__main__":',
90
+ ]
91
+
92
+ [tool.ruff]
93
+ target-version = "py38"
94
+ line-length = 88
95
+ unsafe-fixes = true
96
+ ignore = [
97
+ "D203", # 1 blank line required before class docstring
98
+ "D212", # Multi-line docstring summary should start at the first line
99
+ "D100", # Missing docstring in public module
100
+ "D104", # Missing docstring in public package
101
+ "D107", # Missing docstring in `__init__`
102
+ "D401", # First line of docstring should be in imperative mood
103
+ "D101",
104
+ "D102",
105
+ ]
106
+ select = [
107
+ "B", # flake8-bugbear
108
+ "D", # flake8-docstrings
109
+ "C4", # flake8-comprehensions
110
+ "S", # flake8-bandit
111
+ "F", # pyflake
112
+ "E", # pycodestyle
113
+ "W", # pycodestyle
114
+ "UP", # pyupgrade
115
+ "I", # isort
116
+ "RUF", # ruff specific
117
+ ]
118
+
119
+ [tool.ruff.per-file-ignores]
120
+ "tests/**/*" = [
121
+ "D100",
122
+ "D101",
123
+ "D102",
124
+ "D103",
125
+ "D104",
126
+ "S101",
127
+ ]
128
+ "setup.py" = ["D100"]
129
+ "conftest.py" = ["D100"]
130
+ "docs/conf.py" = ["D100"]
131
+
132
+ [tool.ruff.isort]
133
+ known-first-party = ["vr180_convert", "tests"]
134
+
135
+ [tool.mypy]
136
+ check_untyped_defs = true
137
+ disallow_any_generics = true
138
+ disallow_incomplete_defs = true
139
+ disallow_untyped_defs = true
140
+ mypy_path = "src/"
141
+ no_implicit_optional = true
142
+ show_error_codes = true
143
+ warn_unreachable = true
144
+ warn_unused_ignores = true
145
+ exclude = [
146
+ 'docs/.*',
147
+ 'setup.py',
148
+ ]
149
+
150
+ [[tool.mypy.overrides]]
151
+ module = "tests.*"
152
+ allow_untyped_defs = true
153
+
154
+ [[tool.mypy.overrides]]
155
+ module = "docs.*"
156
+ ignore_errors = true
157
+
158
+ [build-system]
159
+ requires = ["poetry-core>=1.0.0"]
160
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,32 @@
1
+ __version__ = "0.1.0"
2
+ from .remapper import apply, apply_lr, get_map
3
+ from .transformer import (
4
+ DenormalizeTransformer,
5
+ EquirectangularEncoder,
6
+ Euclidean3DRotator,
7
+ Euclidean3DTransformer,
8
+ FisheyeDecoder,
9
+ FisheyeEncoder,
10
+ MultiTransformer,
11
+ NormalizeTransformer,
12
+ PolarRollTransformer,
13
+ TransformerBase,
14
+ ZoomTransformer,
15
+ )
16
+
17
+ __all__ = [
18
+ "TransformerBase",
19
+ "ZoomTransformer",
20
+ "MultiTransformer",
21
+ "NormalizeTransformer",
22
+ "PolarRollTransformer",
23
+ "DenormalizeTransformer",
24
+ "FisheyeDecoder",
25
+ "FisheyeEncoder",
26
+ "EquirectangularEncoder",
27
+ "Euclidean3DRotator",
28
+ "Euclidean3DTransformer",
29
+ "apply",
30
+ "apply_lr",
31
+ "get_map",
32
+ ]
@@ -0,0 +1,5 @@
1
+ """Make the CLI runnable using python -m vr180_convert."""
2
+
3
+ from .cli import app
4
+
5
+ app(prog_name="vr180-convert")
@@ -0,0 +1,148 @@
1
+ from enum import auto
2
+ from pathlib import Path
3
+
4
+ import cv2 as cv
5
+ import typer
6
+ from quaternion import * # noqa
7
+ from strenum import StrEnum
8
+ from typing_extensions import Annotated
9
+
10
+ from vr180_convert.transformer import * # noqa
11
+ from vr180_convert.transformer import EquirectangularEncoder, FisheyeDecoder
12
+
13
+ from .remapper import apply, apply_lr
14
+
15
+ DEFAULT_EXTENSION = ".png"
16
+
17
+ app = typer.Typer()
18
+
19
+
20
+ class _InterpolationFlags(StrEnum):
21
+ """Interpolation flags enum for typer."""
22
+
23
+ INTER_NEAREST = auto()
24
+ INTER_LINEAR = auto()
25
+ INTER_CUBIC = auto()
26
+ INTER_AREA = auto()
27
+ INTER_LANCZOS4 = auto()
28
+ INTER_MAX = auto()
29
+ WARP_FILL_OUTLIERS = auto()
30
+ WARP_INVERSE_MAP = auto()
31
+
32
+
33
+ class _BorderTypes(StrEnum):
34
+ """Border types enum for typer."""
35
+
36
+ BORDER_CONSTANT = auto()
37
+ BORDER_REPLICATE = auto()
38
+ BORDER_REFLECT = auto()
39
+ BORDER_WRAP = auto()
40
+ BORDER_REFLECT_101 = auto()
41
+ BORDER_TRANSPARENT = auto()
42
+ BORDER_ISOLATED = auto()
43
+
44
+
45
+ @app.command()
46
+ def lr(
47
+ left_path: Annotated[Path, typer.Argument(help="Left image path")],
48
+ right_path: Annotated[Path, typer.Argument(help="Right image path")],
49
+ transformer: Annotated[
50
+ str, typer.Option(help="Transformer Python code (to be `eval()`ed)")
51
+ ] = "",
52
+ out_path: Annotated[
53
+ Path,
54
+ typer.Option(
55
+ help="Output image path, defaults to left_path.with_suffix('.out.jpg')"
56
+ ),
57
+ ] = Path(""),
58
+ size: Annotated[
59
+ str, typer.Option(help="Output image size, defaults to 2048x2048")
60
+ ] = "2048x2048",
61
+ interpolation: Annotated[
62
+ _InterpolationFlags,
63
+ typer.Option(help="Interpolation method, defaults to lanczos4"),
64
+ ] = _InterpolationFlags.INTER_LANCZOS4, # type: ignore
65
+ boarder_mode: Annotated[
66
+ _BorderTypes, typer.Option(help="Border mode, defaults to constant")
67
+ ] = _BorderTypes.BORDER_CONSTANT, # type: ignore
68
+ boarder_value: int = 0,
69
+ radius: Annotated[
70
+ str, typer.Option(help="Radius of the fisheye image, defaults to 'auto'")
71
+ ] = "auto",
72
+ ) -> None:
73
+ """Remap a pair of fisheye images to a pair of SBS equirectangular images."""
74
+ if transformer == "":
75
+ transformer_ = EquirectangularEncoder() * FisheyeDecoder("equidistant")
76
+ else:
77
+ transformer_ = eval(transformer) # noqa
78
+ apply_lr(
79
+ transformer=transformer_,
80
+ left_path=left_path,
81
+ right_path=right_path,
82
+ out_path=(
83
+ Path(left_path).with_suffix(f".out.{DEFAULT_EXTENSION}")
84
+ if out_path == Path("")
85
+ else out_path
86
+ ),
87
+ radius=float(radius) if radius not in ["auto", "max"] else radius, # type: ignore
88
+ size_output=tuple(map(int, size.split("x"))), # type: ignore
89
+ interpolation=getattr(cv, interpolation.upper()),
90
+ boarder_mode=getattr(cv, boarder_mode.upper()),
91
+ boarder_value=boarder_value,
92
+ )
93
+
94
+
95
+ @app.command()
96
+ def s(
97
+ in_paths: Annotated[list[Path], typer.Argument(help="Image paths")],
98
+ transformer: Annotated[
99
+ str, typer.Option(help="Transformer Python code (to be `eval()`ed)")
100
+ ] = "",
101
+ out_path: Annotated[
102
+ Path,
103
+ typer.Option(
104
+ help="Output image path, defaults to left_path.with_suffix('.out.jpg')"
105
+ ),
106
+ ] = Path(""),
107
+ size: Annotated[
108
+ str, typer.Option(help="Output image size, defaults to 2048x2048")
109
+ ] = "2048x2048",
110
+ interpolation: Annotated[
111
+ _InterpolationFlags,
112
+ typer.Option(help="Interpolation method, defaults to lanczos4"),
113
+ ] = _InterpolationFlags.INTER_LANCZOS4, # type: ignore
114
+ boarder_mode: Annotated[
115
+ _BorderTypes, typer.Option(help="Border mode, defaults to constant")
116
+ ] = _BorderTypes.BORDER_CONSTANT, # type: ignore
117
+ boarder_value: int = 0,
118
+ radius: Annotated[
119
+ str, typer.Option(help="Radius of the fisheye image, defaults to 'auto'")
120
+ ] = "auto",
121
+ ) -> None:
122
+ """Remap fisheye images to SBS equirectangular images."""
123
+ if transformer == "":
124
+ transformer_ = EquirectangularEncoder() * FisheyeDecoder("equidistant")
125
+ else:
126
+ transformer_ = eval(transformer) # noqa
127
+
128
+ if out_path == Path(""):
129
+ out_paths = [p.with_suffix(f".out.{DEFAULT_EXTENSION}") for p in in_paths]
130
+ elif out_path.is_dir():
131
+ out_paths = [out_path / p.name for p in in_paths]
132
+ else:
133
+ if len(in_paths) > 1:
134
+ raise ValueError(
135
+ "Output path must be a directory when multiple input paths are provided"
136
+ )
137
+ out_paths = [out_path for p in in_paths]
138
+
139
+ apply(
140
+ transformer=transformer_,
141
+ in_paths=in_paths,
142
+ out_paths=out_paths,
143
+ radius=float(radius) if radius not in ["auto", "max"] else radius, # type: ignore
144
+ size_output=tuple(map(int, size.split("x"))), # type: ignore
145
+ interpolation=getattr(cv, interpolation.upper()),
146
+ boarder_mode=getattr(cv, boarder_mode.upper()),
147
+ boarder_value=boarder_value,
148
+ )
File without changes
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Literal, Sequence
5
+
6
+ import cv2 as cv
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+ from .transformer import (
11
+ DenormalizeTransformer,
12
+ NormalizeTransformer,
13
+ TransformerBase,
14
+ get_radius,
15
+ )
16
+
17
+
18
+ def get_map(
19
+ transformer: TransformerBase,
20
+ *,
21
+ radius: float,
22
+ size_input: tuple[int, int],
23
+ size_output: tuple[int, int] = (2048, 2048),
24
+ ) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
25
+ """
26
+ Generate remap map.
27
+
28
+ Parameters
29
+ ----------
30
+ transformer : TransformerBase
31
+ Transformer to be applied.
32
+ radius : float
33
+ Radius of the fisheye image.
34
+ size_input : tuple[int, int]
35
+ Size of the input image.
36
+ size_output : tuple[int, int], optional
37
+ Size of the output image, by default (2048, 2048)
38
+
39
+ Returns
40
+ -------
41
+ tuple[NDArray[np.float32], NDArray[np.float32]]
42
+ xmap and ymap.
43
+
44
+ """
45
+ xmap, ymap = np.meshgrid(np.arange(size_output[0]), np.arange(size_output[1]))
46
+ xmap, ymap = (
47
+ NormalizeTransformer()
48
+ * transformer
49
+ * DenormalizeTransformer(
50
+ scale=(radius, radius), center=(size_input[0] // 2, size_input[1] // 2)
51
+ )
52
+ ).transform(xmap, ymap)
53
+ xmap, ymap = xmap.astype(np.float32), ymap.astype(np.float32)
54
+ return xmap, ymap
55
+
56
+
57
+ def apply(
58
+ transformer: TransformerBase,
59
+ *,
60
+ in_paths: Sequence[Path | str] | Path | str,
61
+ out_paths: Sequence[Path | str] | None | Path | str = None,
62
+ size_output: tuple[int, int] = (2048, 2048),
63
+ interpolation: int = cv.INTER_LANCZOS4,
64
+ boarder_mode: int = cv.BORDER_CONSTANT,
65
+ boarder_value: int | tuple[int, int, int] = 0,
66
+ radius: float | Literal["auto", "max"] = "auto",
67
+ ) -> Sequence[NDArray[np.uint8]]:
68
+ """
69
+ Apply transformer to images.
70
+
71
+ Parameters
72
+ ----------
73
+ transformer : TransformerBase
74
+ Transformer to be applied.
75
+ in_paths : Sequence[Path | str] | Path | str
76
+ Input image paths.
77
+ out_paths : Sequence[Path | str] | None | Path | str, optional
78
+ Output image paths, by default None
79
+ If None, do not save the images.
80
+ size_output : tuple[int, int], optional
81
+ Size of the output image, by default (2048, 2048)
82
+ interpolation : int, optional
83
+ Interpolation method for opencv, by default cv.INTER_LANCZOS4
84
+ boarder_mode : int, optional
85
+ Boarder mode for opencv, by default cv.BORDER_CONSTANT
86
+ boarder_value : int | tuple[int, int, int], optional
87
+ Boarder value for opencv, by default 0
88
+ radius : float | Literal[&quot;auto&quot;, &quot;max&quot;], optional
89
+ Radius of the fisheye image, by default &quot;auto&quot;
90
+
91
+ Returns
92
+ -------
93
+ Sequence[NDArray[np.uint8]]
94
+ Images after transformation.
95
+
96
+ """
97
+ # note that str is Sequence
98
+ in_paths_ = [in_paths] if isinstance(in_paths, (str, Path)) else in_paths
99
+ out_paths_ = [out_paths] if isinstance(out_paths, (str, Path)) else out_paths
100
+ del in_paths, out_paths
101
+
102
+ images = [cv.imread(Path(from_path).as_posix()) for from_path in in_paths_]
103
+ if radius == "auto":
104
+ radius_candidates = [get_radius(image) for image in images]
105
+ radius_ = max(radius_candidates)
106
+ elif radius == "max":
107
+ radius_ = min(images[0].shape[0] / 2, images[0].shape[1] / 2)
108
+ else:
109
+ radius_ = radius
110
+
111
+ xmap, ymap = get_map(
112
+ radius=radius_,
113
+ transformer=transformer,
114
+ size_output=size_output,
115
+ size_input=(images[0].shape[0], images[0].shape[1]),
116
+ )
117
+
118
+ images = [
119
+ cv.remap(
120
+ img,
121
+ xmap,
122
+ ymap,
123
+ interpolation=interpolation,
124
+ borderMode=boarder_mode,
125
+ borderValue=boarder_value,
126
+ )
127
+ for img in images
128
+ ]
129
+
130
+ if out_paths_ is not None:
131
+ for to_path, image in zip(out_paths_, images):
132
+ cv.imwrite(Path(to_path).as_posix(), image)
133
+ return images
134
+
135
+
136
+ def apply_lr(
137
+ transformer: TransformerBase,
138
+ *,
139
+ left_path: Path | str,
140
+ right_path: Path | str,
141
+ out_path: Path | str,
142
+ size_output: tuple[int, int] = (2048, 2048),
143
+ interpolation: int = cv.INTER_LANCZOS4,
144
+ boarder_mode: int = cv.BORDER_CONSTANT,
145
+ boarder_value: int | tuple[int, int, int] = 0,
146
+ radius: float | Literal["auto", "max"] = "auto",
147
+ ) -> None:
148
+ """
149
+ Apply transformer to a pair of images.
150
+
151
+ Parameters
152
+ ----------
153
+ transformer : TransformerBase
154
+ Transformer to be applied.
155
+ left_path : Path | str
156
+ Left image path.
157
+ right_path : Path | str
158
+ Right image path.
159
+ in_paths : Sequence[Path | str] | Path | str
160
+ Input image paths.
161
+ out_path : Path | str
162
+ Output image path.
163
+ size_output : tuple[int, int], optional
164
+ Size of the output image, by default (2048, 2048)
165
+ interpolation : int, optional
166
+ Interpolation method for opencv, by default cv.INTER_LANCZOS4
167
+ boarder_mode : int, optional
168
+ Boarder mode for opencv, by default cv.BORDER_CONSTANT
169
+ boarder_value : int | tuple[int, int, int], optional
170
+ Boarder value for opencv, by default 0
171
+ radius : float | Literal[&quot;auto&quot;, &quot;max&quot;], optional
172
+ Radius of the fisheye image, by default &quot;auto&quot;
173
+
174
+ """
175
+ images = apply(
176
+ in_paths=[left_path, right_path],
177
+ out_paths=None,
178
+ transformer=transformer,
179
+ size_output=size_output,
180
+ interpolation=interpolation,
181
+ boarder_mode=boarder_mode,
182
+ boarder_value=boarder_value,
183
+ radius=radius,
184
+ )
185
+ combine = np.concatenate(images, axis=1)
186
+ cv.imwrite(Path(out_path).as_posix(), combine)
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from colorsys import hls_to_rgb
4
+ from pathlib import Path
5
+
6
+ import cv2 as cv
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+
10
+
11
+ def generate_test_image(
12
+ size: int = 2048, path: str | Path | None = None
13
+ ) -> NDArray[np.uint8]:
14
+ """
15
+ Generate a test image.
16
+
17
+ Parameters
18
+ ----------
19
+ size : int, optional
20
+ The height and width of the image, by default 2048
21
+ path : str | Path | None, optional
22
+ The path to save the image, by default None
23
+ If None, the image will not be saved.
24
+
25
+ Returns
26
+ -------
27
+ NDArray[np.uint8]
28
+ The generated image.
29
+
30
+ """
31
+ img = np.zeros((size, size, 3), dtype=np.uint8)
32
+ center = size // 2
33
+ scale = size // 512 + 1
34
+ # draw circle
35
+ for radius in np.linspace(0, center, 10, endpoint=True):
36
+ color = hls_to_rgb(radius / center, 0.5, 1)
37
+ color = tuple(int(c * 255) for c in color) # type: ignore
38
+ cv.circle(img, (center, center), int(radius), color, scale)
39
+ for angle in np.linspace(0, np.pi * 2, 4, endpoint=False):
40
+ cv.putText(
41
+ img,
42
+ f"{radius / center * 90:g}",
43
+ (
44
+ int(center + np.cos(angle) * radius),
45
+ int(center + np.sin(angle) * radius),
46
+ ),
47
+ cv.FONT_HERSHEY_SIMPLEX,
48
+ scale // 2,
49
+ color,
50
+ scale // 2,
51
+ )
52
+ # draw line
53
+ for angle in np.linspace(0, np.pi * 2, 24, endpoint=False):
54
+ color = hls_to_rgb(angle / (np.pi * 2), 0.5, 1)
55
+ color = tuple(int(c * 255) for c in color) # type: ignore
56
+ x = center + np.cos(angle) * center
57
+ y = center + np.sin(angle) * center
58
+ cv.line(img, (center, center), (int(x), int(y)), color, scale)
59
+ if path:
60
+ cv.imwrite(Path(path).as_posix(), img)
61
+ return img
@@ -0,0 +1,392 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from typing import Any, Literal, Sequence
5
+
6
+ import attrs
7
+ import numpy as np
8
+ from numpy.typing import NDArray
9
+ from quaternion import quaternion, rotate_vectors
10
+ from sklearn.base import BaseEstimator, TransformerMixin
11
+
12
+
13
+ class TransformerBase(
14
+ BaseEstimator,
15
+ TransformerMixin,
16
+ metaclass=ABCMeta,
17
+ ):
18
+ """Base class for transformers."""
19
+
20
+ # def fit(self, image: NDArray, **kwargs: Any) -> None:
21
+ # pass
22
+
23
+ @abstractmethod
24
+ def transform(
25
+ self, x: NDArray, y: NDArray, **kwargs: Any
26
+ ) -> tuple[NDArray, NDArray]:
27
+ """
28
+ Transform the input coordinates.
29
+
30
+ Parameters
31
+ ----------
32
+ x : NDArray
33
+ x (left-right) coordinates.
34
+ y : NDArray
35
+ y (up-down) coordinates.
36
+ **kwargs : Any
37
+ Any additional keyword arguments.
38
+
39
+ Returns
40
+ -------
41
+ tuple[NDArray, NDArray]
42
+ x and y coordinates after transformation.
43
+
44
+ """
45
+ pass
46
+
47
+ # @abstractmethod
48
+ # def inverse_transform(self, x: NDArray, y: NDArray, **kwargs: Any)
49
+ # -> tuple[NDArray, NDArray]:
50
+ # pass
51
+
52
+ def __mul__(self, other: TransformerBase) -> MultiTransformer:
53
+ """Multiply two transformers together."""
54
+ if isinstance(self, MultiTransformer) and isinstance(other, MultiTransformer):
55
+ return MultiTransformer(
56
+ transformers=[*self.transformers, *other.transformers]
57
+ )
58
+ if isinstance(self, MultiTransformer):
59
+ return MultiTransformer(transformers=[*self.transformers, other])
60
+ if isinstance(other, MultiTransformer):
61
+ return MultiTransformer(transformers=[self, *other.transformers])
62
+ return MultiTransformer(transformers=[self, other])
63
+
64
+
65
+ @attrs.define()
66
+ class MultiTransformer(TransformerBase):
67
+ """A transformer that applies multiple transformers in sequence."""
68
+
69
+ transformers: list[TransformerBase]
70
+
71
+ def transform(
72
+ self, x: NDArray, y: NDArray, **kwargs: Any
73
+ ) -> tuple[NDArray, NDArray]:
74
+ # print(f"{y[:, y.shape[0] // 2].max()=}, {x[x.shape[1] // 2, :].max()=}")
75
+
76
+ for transformer in self.transformers:
77
+ x, y = transformer.transform(x, y, **kwargs)
78
+ # print(
79
+ # f"{transformer=}, {y[:, y.shape[0] // 2].max()=}, "
80
+ # f"{x[x.shape[1] // 2, :].max()=}"
81
+ # )
82
+ return x, y
83
+
84
+ # def inverse_transform(self, x: NDArray, y: NDArray,
85
+ # **kwargs: Any) -> tuple[NDArray, NDArray]:
86
+ # for transformer in reversed(self.transformers):
87
+ # x, y = transformer.inverse_transform(x, y, **kwargs)
88
+ # return x, y
89
+
90
+
91
+ def get_radius(input: NDArray, *, threshold: int = 10) -> float:
92
+ """
93
+ Estimate the radius of the circle in the image.
94
+
95
+ Parameters
96
+ ----------
97
+ input : NDArray
98
+ The input image.
99
+ threshold : int, optional
100
+ The threshold to determine if a pixel is black, by default 10
101
+
102
+ Returns
103
+ -------
104
+ float
105
+ The estimated radius.
106
+
107
+ """
108
+ height = input.shape[0]
109
+ center_row = input[height // 2, :, :]
110
+ center_row_is_black = np.mean(center_row, axis=-1) < threshold
111
+ center_row_is_black_deriv = np.diff(center_row_is_black.astype(int))
112
+
113
+ # first and last 1 in the derivative
114
+ center_row_black_start = np.where(center_row_is_black_deriv == 1)[0][0]
115
+ center_row_black_end = np.where(center_row_is_black_deriv == -1)[0][-1]
116
+ radius = (center_row_black_end - center_row_black_start) / 2
117
+ return radius
118
+
119
+
120
+ @attrs.define()
121
+ class NormalizeTransformer(TransformerBase):
122
+ center: tuple[float, float] | None = None
123
+ scale: tuple[float, float] | None = None
124
+
125
+ def transform(
126
+ self, x: NDArray, y: NDArray, **kwargs: Any
127
+ ) -> tuple[NDArray, NDArray]:
128
+ center = self.center or (x.shape[1] / 2, x.shape[0] / 2)
129
+ scale = self.scale or (x.shape[1], x.shape[0])
130
+ x = (x - center[0]) / scale[0] * 2
131
+ y = (y - center[1]) / scale[1] * 2
132
+ return x, y
133
+
134
+ # def inverse_transform(self, x: NDArray, y: NDArray) -> tuple[NDArray, NDArray]:
135
+ # center = self.center or (x.shape[1] / 2, x.shape[0] / 2)
136
+ # scale = self.scale or (x.shape[1] / 2, x.shape[0] / 2)
137
+ # x = x * scale[0] + center[0]
138
+ # y = y * scale[1] + center[1]
139
+ # return x, y
140
+
141
+
142
+ # @attrs.define()
143
+ # class AutoDetectRadiusNormalizeTransformer(NormalizeTransformer):
144
+ # def fit(self, image: NDArray, **kwargs: Any) -> None:
145
+ # radius = get_radius(image)
146
+ # self.center = (image.shape[1] // 2, image.shape[0] // 2)
147
+ # self.scale = (radius, radius)
148
+
149
+
150
+ @attrs.define()
151
+ class DenormalizeTransformer(TransformerBase):
152
+ scale: tuple[float, float]
153
+ center: tuple[float, float]
154
+
155
+ def transform(
156
+ self, x: NDArray, y: NDArray, **kwargs: Any
157
+ ) -> tuple[NDArray, NDArray]:
158
+ scale = self.scale
159
+ center = self.center
160
+ x = x * scale[0] + center[0]
161
+ y = y * scale[1] + center[1]
162
+ return x, y
163
+
164
+ # def inverse_transform(self, x: NDArray, y: NDArray) -> tuple[NDArray, NDArray]:
165
+ # scale = self.scale
166
+ # center = self.center
167
+ # x = (x - center[0]) / scale[0]
168
+ # y = (y - center[1]) / scale[1]
169
+ # return x, y
170
+
171
+
172
+ @attrs.define()
173
+ class PolarRollTransformer(TransformerBase):
174
+ @abstractmethod
175
+ def transform_polar(
176
+ self, theta: NDArray, roll: NDArray, **kwargs: Any
177
+ ) -> tuple[NDArray, NDArray]:
178
+ pass
179
+
180
+ def transform(
181
+ self, x: NDArray, y: NDArray, **kwargs: Any
182
+ ) -> tuple[NDArray, NDArray]:
183
+ theta = np.sqrt(x**2 + y**2)
184
+ roll = np.arctan2(y, x)
185
+ theta, roll = self.transform_polar(theta, roll, **kwargs)
186
+ x = theta * np.cos(roll)
187
+ y = theta * np.sin(roll)
188
+ return x, y
189
+
190
+
191
+ @attrs.define()
192
+ class FisheyeEncoder(PolarRollTransformer):
193
+ mapping_type: Literal[
194
+ "rectilinear", "stereographic", "equidistant", "equisolid", "orthographic"
195
+ ]
196
+
197
+ def transform_polar(
198
+ self, theta: NDArray, roll: NDArray, **kwargs: Any
199
+ ) -> tuple[NDArray, NDArray]:
200
+ """[-1, 1] -> [-pi/2, pi/2]."""
201
+ if self.mapping_type == "rectilinear":
202
+ return np.arctan(theta), roll
203
+ elif self.mapping_type == "stereographic":
204
+ return 2 * np.arctan(theta), roll
205
+ elif self.mapping_type == "equidistant":
206
+ return theta * (np.pi / 2), roll
207
+ elif self.mapping_type == "equisolid":
208
+ return 2 * np.arcsin(theta / np.sqrt(2)), roll
209
+ elif self.mapping_type == "orthographic":
210
+ return np.arcsin(theta), roll
211
+ else:
212
+ raise ValueError(f"Unknown mapping type: {self.mapping_type}")
213
+
214
+
215
+ @attrs.define()
216
+ class FisheyeDecoder(PolarRollTransformer):
217
+ mapping_type: Literal[
218
+ "rectilinear", "stereographic", "equidistant", "equisolid", "orthographic"
219
+ ]
220
+
221
+ def transform_polar(
222
+ self, theta: NDArray, roll: NDArray, **kwargs: Any
223
+ ) -> tuple[NDArray, NDArray]:
224
+ """[-pi/2, pi/2] -> [-1, 1]."""
225
+ if self.mapping_type == "rectilinear":
226
+ return np.tan(theta), roll
227
+ elif self.mapping_type == "stereographic":
228
+ return 2 * np.tan(theta / 2), roll
229
+ elif self.mapping_type == "equidistant":
230
+ return theta / (np.pi / 2), roll
231
+ elif self.mapping_type == "equisolid":
232
+ return np.sqrt(2) * np.sin(theta / 2), roll
233
+ elif self.mapping_type == "orthographic":
234
+ return np.sin(theta), roll
235
+ else:
236
+ raise ValueError(f"Unknown mapping type: {self.mapping_type}")
237
+
238
+
239
+ @attrs.define()
240
+ class PolynomialScaler(PolarRollTransformer):
241
+ coefs_reverse: Sequence[float] = [0, 1]
242
+
243
+ def transform_polar(
244
+ self, theta: NDArray, roll: NDArray, **kwargs: Any
245
+ ) -> tuple[NDArray, NDArray]:
246
+ return np.polyval(np.flip(self.coefs_reverse), theta), roll
247
+
248
+
249
+ @attrs.define()
250
+ class ZoomTransformer(TransformerBase):
251
+ scale: float
252
+
253
+ def transform(
254
+ self, x: NDArray, y: NDArray, **kwargs: Any
255
+ ) -> tuple[NDArray, NDArray]:
256
+ x = x * self.scale
257
+ y = y * self.scale
258
+ return x, y
259
+
260
+
261
+ def equidistant_to_3d(x: NDArray, y: NDArray) -> NDArray:
262
+ """
263
+ Convert 2D coordinates to 3D unit vector.
264
+
265
+ z axis is forward, x axis is right, y axis is up.
266
+
267
+ Parameters
268
+ ----------
269
+ x : NDArray
270
+ The x coordinate in equidistant fisheye format.
271
+ y : NDArray
272
+ The y coordinate in equidistant fisheye format.
273
+
274
+ Returns
275
+ -------
276
+ NDArray
277
+ The 3D unit vector.
278
+
279
+ """
280
+ phi = np.arctan2(x, y)
281
+ theta = np.sqrt(x**2 + y**2)
282
+ v = np.stack(
283
+ [np.sin(theta) * np.sin(phi), np.sin(theta) * np.cos(phi), np.cos(theta)],
284
+ axis=-1,
285
+ )
286
+ return v
287
+
288
+
289
+ def equidistant_from_3d(v: NDArray) -> tuple[NDArray, NDArray]:
290
+ """
291
+ Convert 3D unit vector to 2D coordinates.
292
+
293
+ Parameters
294
+ ----------
295
+ v : NDArray
296
+ The 3D unit vector.
297
+
298
+ Returns
299
+ -------
300
+ tuple[NDArray, NDArray]
301
+ The x and y coordinates in equidistant fisheye format.
302
+
303
+ """
304
+ theta = np.arccos(v[..., 2])
305
+ phi = np.arctan2(v[..., 0], v[..., 1])
306
+ x = theta * np.sin(phi)
307
+ y = theta * np.cos(phi)
308
+ return x, y
309
+
310
+
311
+ @attrs.define()
312
+ class EquirectangularEncoder(TransformerBase):
313
+ is_latitude_y: bool = True
314
+
315
+ def transform(
316
+ self, x: NDArray, y: NDArray, **kwargs: Any
317
+ ) -> tuple[NDArray, NDArray]:
318
+ # latitude: 日本語で緯度, phi
319
+ # longitude: 日本語で経度, theta
320
+ if self.is_latitude_y:
321
+ theta_lat = y * (np.pi / 2)
322
+ phi_lon = x * (np.pi / 2)
323
+ v = np.stack(
324
+ [
325
+ np.cos(theta_lat) * np.sin(phi_lon),
326
+ np.sin(theta_lat),
327
+ np.cos(theta_lat) * np.cos(phi_lon),
328
+ ],
329
+ axis=-1,
330
+ )
331
+ else:
332
+ theta_lat = x * (np.pi / 2)
333
+ phi_lon = y * np.cos(theta_lat) * (np.pi / 2)
334
+ v = np.stack(
335
+ [
336
+ np.sin(theta_lat),
337
+ np.cos(theta_lat) * np.sin(phi_lon),
338
+ np.cos(theta_lat) * np.cos(phi_lon),
339
+ ],
340
+ axis=-1,
341
+ )
342
+
343
+ return equidistant_from_3d(v)
344
+
345
+
346
+ @attrs.define()
347
+ class Euclidean3DTransformer(TransformerBase):
348
+ @abstractmethod
349
+ def transform_v(self, v: NDArray) -> NDArray:
350
+ pass
351
+
352
+ def transform(
353
+ self, x: NDArray, y: NDArray, **kwargs: Any
354
+ ) -> tuple[NDArray, NDArray]:
355
+ v = equidistant_to_3d(x, y)
356
+ print(v.shape)
357
+ v = self.transform_v(v)
358
+ x, y = equidistant_from_3d(v)
359
+ return x, y
360
+
361
+
362
+ @attrs.define()
363
+ class Euclidean3DRotator(Euclidean3DTransformer):
364
+ rotation: quaternion
365
+
366
+ def transform_v(self, v: NDArray) -> NDArray:
367
+ return rotate_vectors(self.rotation, v)
368
+
369
+
370
+ # @attrs.define()
371
+ # class EquirectangularEncoder2(TransformerBase):
372
+ # is_latitude_y: bool = False
373
+
374
+ # def transform(
375
+ # self, x: NDArray, y: NDArray, **kwargs: Any
376
+ # ) -> tuple[NDArray, NDArray]:
377
+ # if self.is_latitude_y:
378
+ # y = y * (np.pi / 2)
379
+ # x = x / np.cos(y) * (np.pi / 2)
380
+ # else:
381
+ # x = x * (np.pi / 2)
382
+ # y = y * (np.pi / 2) * np.cos(x)
383
+ # return x, y
384
+ # def inverse_transform(
385
+ # self, x: NDArray, y: NDArray, **kwargs: Any
386
+ # ) -> tuple[NDArray, NDArray]:
387
+ # v = equidistant_to_3d(x, y)
388
+ # longitude = np.arctan2(v[1], v[0])
389
+ # latitude = np.arcsin(v[2])
390
+ # x = longitude * np.cos(latitude) / (np.pi / 2)
391
+ # y = latitude / (np.pi / 2)
392
+ # return x, y