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.
- vr180_convert-0.1.0/LICENSE +22 -0
- vr180_convert-0.1.0/PKG-INFO +158 -0
- vr180_convert-0.1.0/README.md +124 -0
- vr180_convert-0.1.0/pyproject.toml +160 -0
- vr180_convert-0.1.0/src/vr180_convert/__init__.py +32 -0
- vr180_convert-0.1.0/src/vr180_convert/__main__.py +5 -0
- vr180_convert-0.1.0/src/vr180_convert/cli.py +148 -0
- vr180_convert-0.1.0/src/vr180_convert/py.typed +0 -0
- vr180_convert-0.1.0/src/vr180_convert/remapper.py +186 -0
- vr180_convert-0.1.0/src/vr180_convert/testing.py +61 -0
- vr180_convert-0.1.0/src/vr180_convert/transformer.py +392 -0
|
@@ -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&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
|
+
|  |  |  |
|
|
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&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
|
+
|  |  |  |
|
|
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,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["auto", "max"], optional
|
|
89
|
+
Radius of the fisheye image, by default "auto"
|
|
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["auto", "max"], optional
|
|
172
|
+
Radius of the fisheye image, by default "auto"
|
|
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
|