diff --git a/Dockerfile b/Dockerfile index ef192ca..3247bdb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,4 @@ RUN apt-get update && apt-get -y install python3-dev python3-pip chafa imagemagi RUN mkdir /opt/imgrot COPY . /opt/imgrot RUN pip3 install -r /opt/imgrot/requirements.txt --break-system-packages -ENTRYPOINT [ "python3", "/opt/imgrot/demo.py" ] +ENTRYPOINT [ "python3", "/opt/imgrot/imgrot.py" ] diff --git a/Makefile b/Makefile index cc2bb2f..1e117a4 100644 --- a/Makefile +++ b/Makefile @@ -26,19 +26,19 @@ clean: docker.clean py-clean docs: docs.vhs docs.jinja #docs.rotations docs.jinja: ${pynchon.run} jinja render README.md.j2 - python demo.py img/icon.png --stream > img/demo.gif + python imgrot.py img/icon.png --stream > img/demo.gif docs.vhs:; PS1="$$ " sh -c "${pynchon.run} vhs apply" docs.rotations: - python demo.py img/graph.png --bg lightblue --rotation x --stream > img/rx.gif - python demo.py img/graph.png --bg lightblue --rotation y --stream > img/ry.gif - python demo.py img/graph.png --bg lightblue --rotation s --stream > img/rs.gif - python demo.py img/graph.png --bg lightblue --rotation j --stream > img/rj.gif - python demo.py img/graph.png --bg lightblue --rotation w --stream > img/rw.gif - python demo.py img/graph.png --bg lightblue --rotation f --stream > img/rf.gif - python demo.py img/graph.png --bg lightblue --rotation exit-ul --stream > img/rul.gif - python demo.py img/graph.png --bg lightblue --rotation exit-ur --stream > img/rur.gif - python demo.py img/graph.png --bg lightblue --rotation exit-lr --stream > img/rlr.gif - python demo.py img/graph.png --bg lightblue --rotation exit-ll --stream > img/rll.gif + python imgrot.py img/graph.png --bg lightblue --rotation x --stream > img/rx.gif + python imgrot.py img/graph.png --bg lightblue --rotation y --stream > img/ry.gif + python imgrot.py img/graph.png --bg lightblue --rotation s --stream > img/rs.gif + python imgrot.py img/graph.png --bg lightblue --rotation j --stream > img/rj.gif + python imgrot.py img/graph.png --bg lightblue --rotation w --stream > img/rw.gif + python imgrot.py img/graph.png --bg lightblue --rotation f --stream > img/rf.gif + python imgrot.py img/graph.png --bg lightblue --rotation exit-ul --stream > img/rul.gif + python imgrot.py img/graph.png --bg lightblue --rotation exit-ur --stream > img/rur.gif + python imgrot.py img/graph.png --bg lightblue --rotation exit-lr --stream > img/rlr.gif + python imgrot.py img/graph.png --bg lightblue --rotation exit-ll --stream > img/rll.gif docker.clean: diff --git a/README.md b/README.md index e8afee4..8b2502b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ imgrot -      +
+ @@ -17,7 +18,6 @@

- ------------------------------------- @@ -27,7 +27,7 @@ A fork / update for the excellent original work at [eborboihuc/rotate_3d](https: The original uses `opencv` to rotate 2d -> 3d. This version adds better CLI parsing, support for python3, newer opencv, ability to animate and render animations with `ffmpeg`, and works via docker. -Besides creating gifs, it can display them inside a terminal, using [chafa](https://github.com/hpjansson/chafa). +Different kinds of rotation are supported as well (see the end of this page for [a gallery](#changing-axis-of-rotation)). Besides creating gifs, it can display them inside a terminal, using [chafa](https://github.com/hpjansson/chafa). See also [the original docs](docs/README.original.md) @@ -48,7 +48,7 @@ pip install -r requirements.txt **Basic usage info follows:** ```bash -Usage: demo.py [OPTIONS] IMG_PATH +Usage: imgrot.py [OPTIONS] IMG_PATH Options: --bg TEXT Background color to pass to chafa @@ -78,7 +78,7 @@ You can also set `LOGLEVEL=debug` for more info. A few examples of usage from docker: -**Saving an animated gif to a file:** +#### Saving an Animation ```bash $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --range 360 --img-shape 200x200 --stream > demo.gif @@ -88,7 +88,7 @@ $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot im

-**Rendering a gif, then displaying it in a terminal-friendly way with chafa:** +#### Terminal-Friendly Display ```bash $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --display --stretch --bg lightblue @@ -98,11 +98,11 @@ $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot im

-Note that this tries to respect transparency in the original image, but for more contrast you can effectively add highlights by passing '--bg' arguments that go through to chafa. +Note that this tries to respect transparency in the original image, but for more contrast with black images on black terminals, you can effectively add highlights by passing '--bg' arguments that go through to chafa. ------------------------------ -**Changing axis of rotation** +#### Changing Axis of Rotation The rotation can be controlled to create a bunch of different effects: diff --git a/README.md.j2 b/README.md.j2 index e847f80..bcc9b66 100644 --- a/README.md.j2 +++ b/README.md.j2 @@ -3,7 +3,8 @@ imgrot -      +
+ @@ -17,7 +18,6 @@

- ------------------------------------- @@ -27,7 +27,7 @@ A fork / update for the excellent original work at [eborboihuc/rotate_3d](https: The original uses `opencv` to rotate 2d -> 3d. This version adds better CLI parsing, support for python3, newer opencv, ability to animate and render animations with `ffmpeg`, and works via docker. -Besides creating gifs, it can display them inside a terminal, using [chafa](https://github.com/hpjansson/chafa). +Different kinds of rotation are supported as well (see the end of this page for [a gallery](#changing-axis-of-rotation)). Besides creating gifs, it can display them inside a terminal, using [chafa](https://github.com/hpjansson/chafa). See also [the original docs](docs/README.original.md) @@ -48,7 +48,7 @@ pip install -r requirements.txt **Basic usage info follows:** ```bash -{{bash('python demo.py --help')}} +{{bash('python imgrot.py --help')}} ``` You can also set `LOGLEVEL=debug` for more info. @@ -59,7 +59,7 @@ You can also set `LOGLEVEL=debug` for more info. A few examples of usage from docker: -**Saving an animated gif to a file:** +#### Saving an Animation ```bash $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --range 360 --img-shape 200x200 --stream > demo.gif @@ -69,7 +69,7 @@ $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot im

-**Rendering a gif, then displaying it in a terminal-friendly way with chafa:** +#### Terminal-Friendly Display ```bash $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --display --stretch --bg lightblue @@ -79,11 +79,11 @@ $ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot im

-Note that this tries to respect transparency in the original image, but for more contrast you can effectively add highlights by passing '--bg' arguments that go through to chafa. +Note that this tries to respect transparency in the original image, but for more contrast with black images on black terminals, you can effectively add highlights by passing '--bg' arguments that go through to chafa. ------------------------------ -**Changing axis of rotation** +#### Changing Axis of Rotation The rotation can be controlled to create a bunch of different effects: diff --git a/docs/README.original.md b/docs/README.original.md index 49dd1cc..731cb7f 100644 --- a/docs/README.original.md +++ b/docs/README.original.md @@ -19,7 +19,7 @@ Rotate along XZ axis Change main function with ideal [arguments](#parameters) ```bash -python demo.py [path of the image] [degree to rotate] ([ideal width] [ideal height]) +python imgrot.py [path of the image] [degree to rotate] ([ideal width] [ideal height]) ``` e.g., Example of rotating an image along yz-axis from 0 to 360 degree with a 5 pixel shift in +X direction @@ -32,7 +32,7 @@ Example of rotating an image along yz-axis from 0 to 360 degree with a 5 pixel s ``` Then ```bash -python demo.py img/000001.jpg 360 +python imgrot.py img/000001.jpg 360 ``` ## Parameters: diff --git a/docs/tape/demo.tape b/docs/tape/demo.tape index e443186..f5629d3 100644 --- a/docs/tape/demo.tape +++ b/docs/tape/demo.tape @@ -9,7 +9,7 @@ Set Height 800 Set TypingSpeed .05 Set PlaybackSpeed 1 Set CursorBlink false -Type "python demo.py img/icon.png --display --stretch --bg lightblue" +Type "python imgrot.py img/icon.png --display --stretch --bg lightblue" Sleep 1.1 Enter Sleep 30 diff --git a/image_transformer.py b/image_transformer.py index 31d0409..bc154ee 100644 --- a/image_transformer.py +++ b/image_transformer.py @@ -1,8 +1,3 @@ -from math import pi - -import cv2 -import numpy as np - # Usage: # Change main function with ideal arguments # Then @@ -25,6 +20,13 @@ # 1. : http://stackoverflow.com/questions/17087446/how-to-calculate-perspective-transform-for-opencv-from-rotation-angles # 2. : http://jepsonsblog.blogspot.tw/2012/11/rotation-in-3d-using-opencvs.html +import math +import random +from math import pi + +import cv2 +import numpy as np + def get_rad(theta, phi, gamma): return (deg_to_rad(theta), deg_to_rad(phi), deg_to_rad(gamma)) @@ -43,16 +45,20 @@ def rad_to_deg(rad): class ImageTransformer: - """Perspective transformation class for image - with shape (height, width, #channels)""" + """ + Perspective transformation class for image + with shape (height, width, #channels) + """ def load_image(self, img_path, shape=None): + """ """ img = cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if shape is not None: img = cv2.resize(img, shape) return img def __init__(self, image_path, shape): + """ """ self.image_path = image_path self.image = self.load_image(image_path, shape) @@ -60,12 +66,81 @@ def __init__(self, image_path, shape): self.width = self.image.shape[1] self.num_channels = self.image.shape[2] + def get_rotation_args(self, rotation: str, ang: int): + """ + Generates the arguments used for 'rotate_along_axis' + This takes a rotation-mode 'rotation' and an angle + named 'ang' in (0 - rot_range), and is called from a loop. + """ + if rotation in ["y"]: + # y-axis from 0-360 degree, 5 pixel shift in +X + rotargs = dict(phi=ang, dx=5) + elif rotation in ["x"]: + rotargs = dict(gamma=ang) + elif rotation in ["s", "swivel"]: + # yz-axis from 0 to 360 degree + rotargs = dict(phi=ang, gamma=ang) + elif rotation in ["jitter", "j"]: + rotargs = dict( + dx=random.choice([5, 0, 10]), + phi=random.choice([ang, -ang]), + gamma=random.choice([0, ang, -ang]), + ) + elif rotation in ["wobble", "w"]: + rotargs = dict( + dx=random.choice([0, 10, 30]), + dy=random.choice([25, 0, 10]), + phi=random.choice([ang, -ang, math.sin(ang)]), + gamma=random.choice([ang, -ang]), + ) + elif rotation in ["f", "flip"]: + rotargs = dict(dx=ang, dy=-ang, phi=math.tan(ang), gamma=ang) + elif rotation.startswith("q"): + rotargs = dict( + dx=random.choice([0, 5, 10]), + dy=random.choice([25, 0, 10]), + phi=random.choice([ang, -ang, math.sin(ang)]), + gamma=random.choice([ang, -ang]), + ) + elif rotation.startswith("exit"): + direction = rotation.split("-")[1] + if direction == "ul": + rotargs = dict( + dx=-ang, + dy=-ang, + ) + elif direction == "ur": + rotargs = dict( + dx=ang, + dy=-ang, + ) + elif direction == "lr": + rotargs = dict( + dx=ang, + dy=ang, + ) + elif direction == "ll": + rotargs = dict( + dx=-ang, + dy=ang, + ) + else: + raise ValueError(f"unknown rotation: {rotation}") + rotargs.update(phi=math.tan(ang), gamma=math.tan(ang)) + else: + raise ValueError(f"Not sure how to perform rotation {rotation}") + return rotargs + @staticmethod def save_image(img_path, img): + """ + Saves an output image + This is usually one that's resulted from a rotation, i.e a single frame in a larger animation + """ cv2.imwrite(img_path, img) def rotate_along_axis(self, theta=0, phi=0, gamma=0, dx=0, dy=0, dz=0): - """Wrapper for rotating a image""" + """Returns the new image that results from rotating self.image""" # Get radius of rotation along 3 axes rtheta, rphi, rgamma = get_rad(theta, phi, gamma) @@ -78,13 +153,10 @@ def rotate_along_axis(self, theta=0, phi=0, gamma=0, dx=0, dy=0, dz=0): # Get projection matrix mat = self.get_M(rtheta, rphi, rgamma, dx, dy, dz) - return cv2.warpPerspective(self.image.copy(), mat, (self.width, self.height)) - """ Get Perspective Projection Matrix """ - def get_M(self, theta, phi, gamma, dx, dy, dz): - + """Get Perspective Projection Matrix""" w = self.width h = self.height f = self.focal diff --git a/img/demo.chafa.gif b/img/demo.chafa.gif index 0719118..1b3c1bf 100644 Binary files a/img/demo.chafa.gif and b/img/demo.chafa.gif differ diff --git a/demo.py b/imgrot.py similarity index 64% rename from demo.py rename to imgrot.py index c1437f2..9f56ed2 100644 --- a/demo.py +++ b/imgrot.py @@ -2,10 +2,10 @@ # Usage: # Change main function with ideal arguments # then -# python demo.py [name of the image] [degree to rotate] ([ideal width] [ideal height]) +# python imgrot.py [name of the image] [degree to rotate] ([ideal width] [ideal height]) # e.g., -# python demo.py img/000001.jpg 360 -# python demo.py img/000001.jpg 45 500 700 +# python imgrot.py img/000001.jpg 360 +# python imgrot.py img/000001.jpg 45 500 700 # # Parameters: # img_path : path of image that you want rotated @@ -27,15 +27,17 @@ from image_transformer import ImageTransformer -# Read log level from environment variable +# Setup logging log_level = os.getenv("LOGLEVEL", "INFO").upper() logging.basicConfig( level=getattr(logging, log_level, logging.INFO), format="%(asctime)s - imgrot - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stderr)], ) logger = logging.getLogger(__name__) +# Setup CLI interface @click.command() @click.option("--bg", default="black", help="Background color to pass to chafa") @click.option( @@ -104,84 +106,19 @@ def run( duration = f"--duration {duration}" if duration else "" invert = invert and "--invert" or "" bg = f"--bg {bg}" - import math if not os.path.exists(img_path): logger.debug(f"{img_path} does not exist!") raise SystemExit(1) - # img_path_base=os.path.basename(img_path) - # err = os.system(f"convert {img_path} -fuzz 10% -transparent white -alpha off /tmp/{img_path_base}") - # if err: raise SystemExit(1) - # img_path = f"/tmp/{img_path_base}" - rot_range = int(rot_range) img_shape = img_shape and list(map(int, img_shape.split("x"))) it = ImageTransformer(img_path, img_shape) if not os.path.isdir(output_dir): os.mkdir(output_dir) logger.debug(f"Rotating {img_path} .. ") - import random - for ang in range(0, rot_range): - if rotation in ["y"]: - # y-axis from 0-360 degree, 5 pixel shift in +X - rotargs = dict(phi=ang, dx=5) - elif rotation in ["x"]: - rotargs = dict(gamma=ang) - elif rotation in ["s", "swivel"]: - # yz-axis from 0 to 360 degree - rotargs = dict(phi=ang, gamma=ang) - elif rotation in ["jitter", "j"]: - rotargs = dict( - dx=random.choice([5, 0, 10]), - phi=random.choice([ang, -ang]), - gamma=random.choice([0, ang, -ang]), - ) - elif rotation in ["wobble", "w"]: - rotargs = dict( - dx=random.choice([0, 10, 30]), - dy=random.choice([25, 0, 10]), - phi=random.choice([ang, -ang, math.sin(ang)]), - gamma=random.choice([ang, -ang]), - ) - elif rotation in ["f", "flip"]: - rotargs = dict(dx=ang, dy=-ang, phi=math.tan(ang), gamma=ang) - elif rotation.startswith("q"): - rotargs = dict( - dx=random.choice([0, 5, 10]), - dy=random.choice([25, 0, 10]), - phi=random.choice([ang, -ang, math.sin(ang)]), - gamma=random.choice([ang, -ang]), - ) - elif rotation.startswith("exit"): - direction = rotation.split("-")[1] - if direction == "ul": - rotargs = dict( - dx=-ang, - dy=-ang, - ) - elif direction == "ur": - rotargs = dict( - dx=ang, - dy=-ang, - ) - elif direction == "lr": - rotargs = dict( - dx=ang, - dy=ang, - ) - elif direction == "ll": - rotargs = dict( - dx=-ang, - dy=ang, - ) - else: - raise ValueError(f"unknown rotation: {rotation}") - rotargs.update(phi=math.tan(ang), gamma=math.tan(ang)) - else: - raise ValueError(f"Not sure how to perform rotation {rotation}") - rotated_img = it.rotate_along_axis(**rotargs) + rotated_img = it.rotate_along_axis(**it.get_rotation_args(rotation, ang)) fname = f"{output_dir}/{str(ang).zfill(3)}.png" it.save_image(fname, rotated_img) logger.debug("Done") @@ -201,10 +138,10 @@ def run( f"""ffmpeg -y -i /tmp/.tmp.gif -vf "split[s0][s1];[s0]palettegen[p];[s1]setpts={speed}*PTS[v];[v][p]paletteuse" {output_file} {quiet_maybe}""", ] for cmd in commands: - logger.warning(cmd) + logger.debug(cmd) result = subprocess.run(cmd, shell=True, stdout=sys.stderr, stderr=sys.stderr) if result.returncode != 0: - logger.debug(f"Command failed with return code {result.returncode}") + logger.critical(f"Command failed with return code {result.returncode}") raise SystemExit(result.returncode) if view: logger.debug(f"Viewing {img_path}") @@ -223,7 +160,7 @@ def run( content = binary_file.read() sys.stdout.buffer.write(content) else: - logger.debug("No instructions, not sure what to do") + logger.critical("No instructions, not sure what to do") raise SystemExit(1)