Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI Revamp. #30

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Now at everywhere, you can `import py360convert` or use the command line tool `c
You can run command line tool to use the functionality. Please See `convert360 -h` for detailed. The python script is also an example code to see how to use this as a package in your code.

```
convert360 --convert e2c --i assets/example_input.png --o assets/example_e2c.png --w 200
convert360 e2c assets/example_input.png out.png --size 200
```
| Input Equirectangular | Output Cubemap |
| :---: | :----: |
Expand All @@ -28,7 +28,7 @@ convert360 --convert e2c --i assets/example_input.png --o assets/example_e2c.png
-----

```
convert360 --convert c2e --i assets/example_e2c.png --o assets/example_c2e.png --w 800 --h 400
convert360 c2e assets/example_e2c.png out.png --width 800 --height 400
```
| Input Cubemap | Output Equirectangular |
| :---: | :----: |
Expand All @@ -39,7 +39,7 @@ You can see the blurring artifacts in the polar region because the equirectangul
----

```
convert360 --convert e2p --i assets/example_input.png --o assets/example_e2p.png --w 300 --h 300 --u_deg 120 --v_deg 23
convert360 e2p assets/example_input.png out.png --width 300 --height 300 --yaw 120 --pitch 23
```
| Input Equirectangular | Output Perspective |
| :---: | :----: |
Expand All @@ -48,14 +48,33 @@ convert360 --convert e2p --i assets/example_input.png --o assets/example_e2p.png

## Doc

#### `c2e(cubemap, h, w, cube_format='dice')`
Convert the given cubemap to equirectangular.
**Parameters**:
- `cubemap`: Numpy array or list/dict of numpy array (depend on `cube_format`).
- `h`: Output equirectangular height.
- `w`: Output equirectangular width.
- `mode:str`: Interpolation method; typically `bilinear` or `nearest`. Valid options: "nearest", "linear", "bilinear", "biquadratic", "quadratic", "quad", "bicubic", "cubic", "biquartic", "quartic", "biquintic", "quintic".

- `cube_format`: Options: `'dice'` (default), `'horizon'` or `'dict'` or `'list'`. Indicates the format of the given `cubemap`.
- Say that each face of the cube is in shape of `256 (width) x 256 (height)`
- `'dice'`: a numpy array in shape of `1024 x 768` like below example

<img src="assets/cube_dice.png" height="200">
- `'horizon'`: a numpy array in shape of `1536 x 256` like below example

<img src="assets/cube_horizon.png" height="100">
- `'list'`: a `list` with 6 elements each of which is a numpy array in shape of `256 x 256`. It's just converted from 'horizon' format with one line of code: `np.split(cube_h, 6, axis=1)`.
- `'dict'`: a `dict` with 6 elements with keys `'F', 'R', 'B', 'L', 'U', 'D'` each of which is a numpy array in shape of `256 x 256`.
- Please refer to [the source code](https://github.com/sunset1995/py360convert/blob/master/py360convert/utils.py#L176) if you still have question about the conversion between formats.

#### `e2c(e_img, face_w=256, mode='bilinear', cube_format='dice')`
Convert the given equirectangular to cubemap.
**Parameters**:
- `e_img`: Numpy array with shape [H, W, C].
- `face_w`: The width of each cube face.
- `mode`: `bilinear` or `nearest`.
- `cube_format`: See `c2e` explanation.

- `e_img: NDArray`: Numpy array with shape [H, W, C].
- `face_w: int`: The width of each cube face.
- `mode:str`: See `c2e`.
- `cube_format:str`: See `c2e`.

#### `e2p(e_img, fov_deg, u_deg, v_deg, out_hw, in_rot_deg=0, mode='bilinear')`
Take perspective image from given equirectangular.
Expand All @@ -69,24 +88,6 @@ Take perspective image from given equirectangular.
- `mode`: `bilinear` or `nearest`.


#### `c2e(cubemap, h, w, cube_format='dice')`
Convert the given cubemap to equirectangular.
**Parameters**:
- `cubemap`: Numpy array or list/dict of numpy array (depend on `cube_format`).
- `h`: Output equirectangular height.
- `w`: Output equirectangular width.
- `cube_format`: 'dice' (default) or 'horizon' or 'dict' or 'list'. Telling the format of the given `cubemap`.
- Say that each face of the cube is in shape of `256 (width) x 256 (height)`
- 'dice': a numpy array in shape of `1024 x 768` like below example

<img src="assets/cube_dice.png" height="200">
- 'horizon': a numpy array in shape of `1536 x 256` like below example

<img src="assets/cube_horizon.png" height="100">
- 'list': a `list` with 6 elements each of which is a numpy array in shape of `256 x 256`. It's just converted from 'horizon' format with one line of code: `np.split(cube_h, 6, axis=1)`.
- 'dict': a `dict` with 6 elements with keys `'F', 'R', 'B', 'L', 'U', 'D'` each of which is a numpy array in shape of `256 x 256`.
- Please refer to [the source code](https://github.com/sunset1995/py360convert/blob/master/py360convert/utils.py#L176) if you still have question about the conversion between formats.

**Example**:
```python
import numpy as np
Expand Down
142 changes: 108 additions & 34 deletions py360convert/__main__.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,131 @@
import argparse
import sys
from pathlib import Path

import numpy as np
from PIL import Image

import py360convert


def _assert_height_width(args):
if args.height is None:
print("Error: --height required.")
sys.exit(1)
if args.width is None:
print("Error: --width required.")
sys.exit(1)


def _size_to_dims(args):
if args.size is not None:
if args.format == "horizon":
args.height = args.size
args.width = args.size * 6
elif args.format == "dice":
args.height = args.size * 3
args.width = args.size * 4
else:
raise NotImplementedError("")
_assert_height_width(args)


def main():
# Parsing command line arguments
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Conversion between cubemap and equirectangular or equirectangular to perspective.",
description="Conversion between cubemap and equirectangular or equirectangular to perspective.", add_help=False
)
parser.add_argument("--convert", choices=["c2e", "e2c", "e2p"], required=True, help="What conversion to apply.")
parser.add_argument("--i", required=True, help="Path to input image.")
parser.add_argument("--o", required=True, help="Path to output image.")
parser.add_argument(
"--w", required=True, type=int, help="Output width for c2e or e2p. Output cube faces width for e2c."
parser.add_argument("--help", action="help", help="Show this help message and exit")

subparsers = parser.add_subparsers(dest="command", help="Convert command.")

c2e_parser = subparsers.add_parser("c2e", help="Convert cubemap to equirectangular.", add_help=False)
c2e_parser.add_argument("--help", action="help", help="Show this help message and exit")
c2e_parser.add_argument("input", type=Path, help="Path to input image.")
c2e_parser.add_argument("output", type=Path, help="Path to output image.")
c2e_parser.add_argument("--format", "-f", choices=["horizon", "dice"], default="dice", help="Input image layout.")
c2e_parser.add_argument("--height", "-h", type=int, required=True, help="Output image height in pixels.")
c2e_parser.add_argument("--width", "-w", type=int, required=True, help="Output image width in pixels.")
c2e_parser.add_argument(
"--mode", "-m", default="bilinear", choices=["bilinear", "nearest"], help="Resampling method."
)

e2c_parser = subparsers.add_parser("e2c", help="Convert equirectangular to cubemap.", add_help=False)
e2c_parser.add_argument("--help", action="help", help="Show this help message and exit")
e2c_parser.add_argument("input", type=Path, help="Path to input image.")
e2c_parser.add_argument("output", type=Path, help="Path to output image.")
e2c_parser.add_argument("--format", "-f", choices=["horizon", "dice"], default="dice", help="Output image layout.")
e2c_parser.add_argument("--height", "-h", type=int, help="Output image height in pixels.")
e2c_parser.add_argument("--width", "-w", type=int, help="Output image width in pixels.")
e2c_parser.add_argument("--size", "-s", type=int, help="Side length of each cube face. Overrides height/width.")
e2c_parser.add_argument(
"--mode", "-m", default="bilinear", choices=["bilinear", "nearest"], help="Resampling method."
)
parser.add_argument("--h", type=int, help="Output height for c2e or e2p.")
parser.add_argument("--mode", default="bilinear", choices=["bilinear", "nearest"], help="Resampling method.")
parser.add_argument("--h_fov", type=float, default=60, help="Horizontal field of view for e2p.")
parser.add_argument("--v_fov", type=float, default=60, help="Vertical field of view for e2p.")
parser.add_argument("--u_deg", type=float, default=0, help="Horizontal viewing angle for e2p.")
parser.add_argument("--v_deg", type=float, default=0, help="Vertical viewing angle for e2p.")
parser.add_argument("--in_rot_deg", type=float, default=0, help="Inplane rotation for e2p.")

e2p_parser = subparsers.add_parser("e2p", help="Convert equirectangular to perspective.", add_help=False)
e2p_parser.add_argument("--help", action="help", help="Show this help message and exit")
e2p_parser.add_argument("input", type=Path, help="Path to input image.")
e2p_parser.add_argument("output", type=Path, help="Path to output image.")
e2p_parser.add_argument("--height", "-h", type=int, required=True, help="Output image height in pixels.")
e2p_parser.add_argument("--width", "-w", type=int, required=True, help="Output image width in pixels.")
e2p_parser.add_argument("--h-fov", type=float, default=60, help="Horizontal FoV in degrees.")
e2p_parser.add_argument("--v-fov", type=float, default=60, help="Vertical FoV in degrees.")
e2p_parser.add_argument(
"--yaw",
type=float,
default=0,
help="Yaw camera left/right degrees. Positive values rotate right; negative values rotate left.",
)
e2p_parser.add_argument(
"--pitch",
type=float,
default=0,
help="Pitch camera up/down degrees. Positive values pitch up; negative values pitch down.",
)
e2p_parser.add_argument(
"--roll",
type=float,
default=0,
help="Roll camera degrees. Positive values rotate counterclockwise; negative values rotate clockwise.",
)
e2p_parser.add_argument(
"--mode", "-m", default="bilinear", choices=["bilinear", "nearest"], help="Resampling method."
)

args = parser.parse_args()

# Read image
img = np.array(Image.open(args.i))
if len(img.shape) == 2:
img = img[..., None]

# Convert
if args.convert == "c2e":
out = py360convert.c2e(img, h=args.h, w=args.w, mode=args.mode) # pyright: ignore[reportCallIssue]
elif args.convert == "e2c":
out = py360convert.e2c(img, face_w=args.w, mode=args.mode) # pyright: ignore[reportCallIssue]
elif args.convert == "e2p":
if args.command is None:
parser.print_help()
sys.exit(1)
elif args.command == "c2e":
_assert_height_width(args)
img = np.array(Image.open(args.input))
out = py360convert.c2e(img, h=args.height, w=args.width, mode=args.mode, cube_format=args.format) # pyright: ignore[reportCallIssue]
Image.fromarray(out).save(args.output)
elif args.command == "e2c":
_size_to_dims(args)
img = np.array(Image.open(args.input))
out = py360convert.e2c(
img,
face_w=args.width,
mode=args.mode,
cube_format=args.format,
) # pyright: ignore[reportCallIssue]
Image.fromarray(out).save(args.output)
elif args.command == "e2p":
_assert_height_width(args)
img = np.array(Image.open(args.input))
out = py360convert.e2p(
img,
fov_deg=(args.h_fov, args.v_fov),
u_deg=args.u_deg,
v_deg=args.v_deg,
out_hw=(args.h, args.w),
in_rot_deg=args.in_rot_deg,
u_deg=args.yaw,
v_deg=args.pitch,
out_hw=(args.height, args.width),
in_rot_deg=args.roll,
mode=args.mode,
)
Image.fromarray(out).save(args.output)
else:
raise NotImplementedError("Unknown conversion")

# Output image
Image.fromarray(out.astype(np.uint8).squeeze()).save(args.o)
raise NotImplementedError(f'Command "{args.command}" not yet implemented.')


main()
2 changes: 1 addition & 1 deletion py360convert/e2p.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def e2p(
else:
h_fov, v_fov = map(np.deg2rad, fov_deg)

in_rot = in_rot_deg * np.pi / 180
in_rot = np.deg2rad(in_rot_deg)

if mode == "bilinear":
order = 1
Expand Down
Loading