Skip to content

Commit

Permalink
feat: add main feat (#16)
Browse files Browse the repository at this point in the history
* chore: wip

* chore: wip

* chore: wip

* chore: wip

* chore: wip

* chore: wip

* refactor: refactor a lot

* feat: add cli

* chore: update deps

* test: update test

* style: run pre-commit

* fix: fix cli and add more tests

* docs(readme): update README.md

* fix(remapper): fix radius estimation for "max"

* chore: fix last commit

* fix: backward compatibility

* feat(cli): add more commands

* docs(readme): update README.md

* refactor: shorter name

* fix(transformer): remove unnecessary print

* feat: add PolynomialScaler

* feat: fix PolynomialScaler, add image to the docs
  • Loading branch information
34j authored Apr 19, 2024
1 parent 107af8c commit 556aa45
Show file tree
Hide file tree
Showing 15 changed files with 1,340 additions and 50 deletions.
12 changes: 6 additions & 6 deletions .copier-answers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ email: 34j.95a2p@simplelogin.com
full_name: 34j
github_username: 34j
has_cli: true
initial_commit: true
initial_commit: false
open_source_license: MIT
open_with_vscode: true
open_with_vscode: false
package_name: vr180_convert
project_name: VR180 image converter
project_short_description: Simple VR180 image converter
project_slug: vr180-convert
run_poetry_install: true
setup_github: true
setup_pre_commit: true
setup_venv: true
run_poetry_install: false
setup_github: false
setup_pre_commit: false
setup_venv: false
venv_version: '3.11'

61 changes: 59 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,70 @@

---

Simple VR180 image converter
Simple VR180 image converter on top of OpenCV and NumPy.

## Installation

Install this via pip (or your favourite package manager):

`pip install vr180-convert`
```shell
pipx install vr180-convert
```

| Left | Right | Output |
| ------------------------------ | ------------------------------- | ---------------------------------------------------- |
| ![left](docs/_static/test.jpg) | ![right](docs/_static/test.jpg) | ![output](docs/_static/test.lr.PolynomialScaler.jpg) |

## Usage

Simply run the following command to convert 2 fisheye images to a SBS equirectangular VR180 image:

```shell
v1c lr left.jpg right.jpg
```

You can also specify the conversion model by adding Python code directly to the `--transformer` option:

```shell
v1c lr left.jpg right.jpg ---transformer "EquirectangularEncoder() * Euclidean3DRotator(from_rotation_vector([0, np.pi / 4, 0])) * FisheyeDecoder("equidistant")"
```

Please refer to the [API documentation](https://vr180-convert.readthedocs.io/) for the available transformers and their parameters.
For `from_rotation_vector`, please refer to the [numpy-quaternion documentation](https://quaternion.readthedocs.io/en/latest/Package%20API%3A/quaternion/#from_rotation_vector).

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:

```shell
v1c lr left.jpg right.jpg --radius 1000
v1c lr left.jpg right.jpg --radius max # min(width, height) / 2
```

To convert a single image, use `v1c s` instead.

For more information, please refer to the help or API documentation:

```shell
v1c --help
```

## Usage as a library

For more complex transformations, it is recommended to create your own `Transformer`.

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)`.)

```python
from vr180_convert import PolarRollTransformer, apply_lr

class MyTransformer(PolarRollTransformer):
def transform_polar(
self, theta: NDArray, roll: NDArray, **kwargs: Any
) -> tuple[NDArray, NDArray]:
return theta**0.98 + theta**1.01, roll

transformer = EquirectangularEncoder() * MyTransformer() * FisheyeDecoder("equidistant")
apply_lr(transformer, left_path="left.jpg", right_path="right.jpg", out_path="output.jpg")
```

## Contributors ✨

Expand Down
Binary file added docs/_static/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/test.lr.PolynomialScaler.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
322 changes: 298 additions & 24 deletions poetry.lock

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@ packages = [

[tool.poetry.scripts]
vr180-convert = "vr180_convert.cli:app"
v1c = "vr180_convert.cli:app"

[tool.poetry.dependencies]
python = "^3.8"
python = "^3.9"
rich = ">=10"
typer = {extras = ["all"], version = "^0.12.0"}
typer = {extras = ["all"], version = "^0.9.0"}
opencv-python = "^4.9.0.80"
attrs = "^23.2.0"
scikit-learn = "^1.4.2"
numpy-quaternion = "^2023.0.3"
strenum = "^0.4.15"

[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
Expand Down Expand Up @@ -86,13 +92,16 @@ exclude_lines = [
[tool.ruff]
target-version = "py38"
line-length = 88
unsafe-fixes = true
ignore = [
"D203", # 1 blank line required before class docstring
"D212", # Multi-line docstring summary should start at the first line
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
"D107", # Missing docstring in `__init__`
"D401", # First line of docstring should be in imperative mood
"D101",
"D102",
]
select = [
"B", # flake8-bugbear
Expand Down
33 changes: 32 additions & 1 deletion src/vr180_convert/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
__version__ = "0.0.1"
__version__ = "0.0.0"
from .remapper import apply, apply_lr, get_map
from .transformer import (
DenormalizeTransformer,
EquirectangularEncoder,
Euclidean3DRotator,
Euclidean3DTransformer,
FisheyeDecoder,
FisheyeEncoder,
MultiTransformer,
NormalizeTransformer,
PolarRollTransformer,
TransformerBase,
ZoomTransformer,
)

__all__ = [
"TransformerBase",
"ZoomTransformer",
"MultiTransformer",
"NormalizeTransformer",
"PolarRollTransformer",
"DenormalizeTransformer",
"FisheyeDecoder",
"FisheyeEncoder",
"EquirectangularEncoder",
"Euclidean3DRotator",
"Euclidean3DTransformer",
"apply",
"apply_lr",
"get_map",
]
146 changes: 141 additions & 5 deletions src/vr180_convert/cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,148 @@
from enum import auto
from pathlib import Path

import cv2 as cv
import typer
from rich import print
from quaternion import * # noqa
from strenum import StrEnum
from typing_extensions import Annotated

from vr180_convert.transformer import * # noqa
from vr180_convert.transformer import EquirectangularEncoder, FisheyeDecoder

from .main import add
from .remapper import apply, apply_lr

DEFAULT_EXTENSION = ".png"

app = typer.Typer()


class _InterpolationFlags(StrEnum):
"""Interpolation flags enum for typer."""

INTER_NEAREST = auto()
INTER_LINEAR = auto()
INTER_CUBIC = auto()
INTER_AREA = auto()
INTER_LANCZOS4 = auto()
INTER_MAX = auto()
WARP_FILL_OUTLIERS = auto()
WARP_INVERSE_MAP = auto()


class _BorderTypes(StrEnum):
"""Border types enum for typer."""

BORDER_CONSTANT = auto()
BORDER_REPLICATE = auto()
BORDER_REFLECT = auto()
BORDER_WRAP = auto()
BORDER_REFLECT_101 = auto()
BORDER_TRANSPARENT = auto()
BORDER_ISOLATED = auto()


@app.command()
def main(n1: int, n2: int) -> None:
"""Add the arguments and print the result."""
print(add(n1, n2))
def lr(
left_path: Annotated[Path, typer.Argument(help="Left image path")],
right_path: Annotated[Path, typer.Argument(help="Right image path")],
transformer: Annotated[
str, typer.Option(help="Transformer Python code (to be `eval()`ed)")
] = "",
out_path: Annotated[
Path,
typer.Option(
help="Output image path, defaults to left_path.with_suffix('.out.jpg')"
),
] = Path(""),
size: Annotated[
str, typer.Option(help="Output image size, defaults to 2048x2048")
] = "2048x2048",
interpolation: Annotated[
_InterpolationFlags,
typer.Option(help="Interpolation method, defaults to lanczos4"),
] = _InterpolationFlags.INTER_LANCZOS4, # type: ignore
boarder_mode: Annotated[
_BorderTypes, typer.Option(help="Border mode, defaults to constant")
] = _BorderTypes.BORDER_CONSTANT, # type: ignore
boarder_value: int = 0,
radius: Annotated[
str, typer.Option(help="Radius of the fisheye image, defaults to 'auto'")
] = "auto",
) -> None:
"""Remap a pair of fisheye images to a pair of SBS equirectangular images."""
if transformer == "":
transformer_ = EquirectangularEncoder() * FisheyeDecoder("equidistant")
else:
transformer_ = eval(transformer) # noqa
apply_lr(
transformer=transformer_,
left_path=left_path,
right_path=right_path,
out_path=(
Path(left_path).with_suffix(f".out.{DEFAULT_EXTENSION}")
if out_path == Path("")
else out_path
),
radius=float(radius) if radius not in ["auto", "max"] else radius, # type: ignore
size_output=tuple(map(int, size.split("x"))), # type: ignore
interpolation=getattr(cv, interpolation.upper()),
boarder_mode=getattr(cv, boarder_mode.upper()),
boarder_value=boarder_value,
)


@app.command()
def s(
in_paths: Annotated[list[Path], typer.Argument(help="Image paths")],
transformer: Annotated[
str, typer.Option(help="Transformer Python code (to be `eval()`ed)")
] = "",
out_path: Annotated[
Path,
typer.Option(
help="Output image path, defaults to left_path.with_suffix('.out.jpg')"
),
] = Path(""),
size: Annotated[
str, typer.Option(help="Output image size, defaults to 2048x2048")
] = "2048x2048",
interpolation: Annotated[
_InterpolationFlags,
typer.Option(help="Interpolation method, defaults to lanczos4"),
] = _InterpolationFlags.INTER_LANCZOS4, # type: ignore
boarder_mode: Annotated[
_BorderTypes, typer.Option(help="Border mode, defaults to constant")
] = _BorderTypes.BORDER_CONSTANT, # type: ignore
boarder_value: int = 0,
radius: Annotated[
str, typer.Option(help="Radius of the fisheye image, defaults to 'auto'")
] = "auto",
) -> None:
"""Remap fisheye images to SBS equirectangular images."""
if transformer == "":
transformer_ = EquirectangularEncoder() * FisheyeDecoder("equidistant")
else:
transformer_ = eval(transformer) # noqa

if out_path == Path(""):
out_paths = [p.with_suffix(f".out.{DEFAULT_EXTENSION}") for p in in_paths]
elif out_path.is_dir():
out_paths = [out_path / p.name for p in in_paths]
else:
if len(in_paths) > 1:
raise ValueError(
"Output path must be a directory when multiple input paths are provided"
)
out_paths = [out_path for p in in_paths]

apply(
transformer=transformer_,
in_paths=in_paths,
out_paths=out_paths,
radius=float(radius) if radius not in ["auto", "max"] else radius, # type: ignore
size_output=tuple(map(int, size.split("x"))), # type: ignore
interpolation=getattr(cv, interpolation.upper()),
boarder_mode=getattr(cv, boarder_mode.upper()),
boarder_value=boarder_value,
)
3 changes: 0 additions & 3 deletions src/vr180_convert/main.py

This file was deleted.

Loading

0 comments on commit 556aa45

Please sign in to comment.