Skip to content

Commit

Permalink
Merge pull request #30 from sunset1995/new-cli
Browse files Browse the repository at this point in the history
CLI Revamp.
  • Loading branch information
BrianPugh authored Dec 16, 2024
2 parents 57f4781 + 0a0dd50 commit 0997a7d
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 61 deletions.
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

0 comments on commit 0997a7d

Please sign in to comment.