diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..91c2484
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,108 @@
+# Git
+.git
+.gitignore
+.github
+
+# CI
+.codeclimate.yml
+.travis.yml
+.taskcluster.yml
+
+# Docker
+docker-compose.yml
+.docker
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*/__pycache__/
+*/*/__pycache__/
+*/*/*/__pycache__/
+*.py[cod]
+*/*.py[cod]
+*/*/*.py[cod]
+*/*/*/*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+.eggs/
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml
+.tmp.*
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Virtual environment
+.env/
+.venv/
+venv/
+
+# PyCharm
+.idea
+
+# Python mode for VIM
+.ropeproject
+*/.ropeproject
+*/*/.ropeproject
+*/*/*/.ropeproject
+
+# Vim swap files
+*.swp
+*/*.swp
+*/*/*.swp
+*/*/*/*.swp
+
+tox.ini
+Dockerfile
+Makefile
+img/
+build/
+*.md
+*.md.j2
+[.]*
+docs/
+tests/
+LICENSE
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..7b2dcca
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,28 @@
+[flake8]
+exclude = .venv,venv,.tox,dist,doc,build,*.egg,docs,setup.py,*/migrations/
+ignore = W605,W291,E401,E203,E303,E225,E201,E202,E121,E123,E125,E126,E127,E128,E131,E222,E226,E231,E251,E252,E261,E265,E266,E302,E305,E306,E402,E501,E714,E722,F403,F405,F841,W391,W503
+max-line-length = 160
+# E121 continuation line under-indented for hanging indent
+# E123 closing bracket does not match indentation of opening bracket's line
+# E125 continuation line with same indent as next logical line
+# E126 continuation line over-indented for hanging indent
+# E127 continuation line over-indented for visual indent
+# E128 continuation line under-indented for visual indent
+# E131 continuation line unaligned for hanging indent
+# E222 multiple spaces after operator
+# E226 missing whitespace around arithmetic operator
+# E231 missing whitespace after
+# E251 unexpected spaces around keyword / parameter equals
+# E261 at least two spaces before inline comment
+# E265 block comment should start with '# '
+# E302 expected 2 blank lines, found 1
+# E305 expected 2 blank lines after class or function definition, found 1
+# E402 module level import not at top of file
+# E714 test for object identity should be 'is not'
+# E722 do not use bare except
+# F401 Imported but unused
+# F403 'from module import *' used; unable to detect undefined names
+# F405 foo may be undefined, or defined from star imports
+# F841 local variable 'foo' is assigned to but never used
+# W391 blank line at end of file
+# W605 invalid escape sequence
diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml
new file mode 100644
index 0000000..0b339cf
--- /dev/null
+++ b/.github/workflows/docker-build-push.yml
@@ -0,0 +1,26 @@
+name: Publish Docker image
+on:
+ release:
+ types: [published]
+ push:
+ branches: [ "master" ]
+ paths-ignore:
+ - '**.md'
+ - '**.md.j2'
+ - '**.png'
+jobs:
+ push_to_registry:
+ name: Push Docker image to Docker Hub
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v3
+ - name: Build
+ run: make docker.build
+ - name: Log in to Docker Hub
+ uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ - name: Push
+ run: make docker.push
\ No newline at end of file
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000..9988c84
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,17 @@
+name: Docker Build
+on:
+ workflow_call: {}
+ pull_request:
+ branches: [ "master" ]
+ paths-ignore:
+ - '**.md'
+permissions:
+ contents: read
+jobs:
+ test:
+ name: Docker Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Build
+ run: make clean build test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 6e9cfbd..6072aaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,137 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
-output/
-*.pyc
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+src/*/_version.py
+.DS_Store
+
+# Ignore dynaconf secret files
+.secrets.*
+tests/fixtures/graph.png
+.tmp.*
+_version.py
diff --git a/.isort.cfg b/.isort.cfg
new file mode 100644
index 0000000..345638d
--- /dev/null
+++ b/.isort.cfg
@@ -0,0 +1,7 @@
+# https://github.com/PyCQA/isort
+[isort]
+#profile="black"
+length_sort=True
+# changing import order here can introduce import-errors.
+no_inline_sort=True
+honor_noqa=True
diff --git a/.pynchon.json5 b/.pynchon.json5
new file mode 100644
index 0000000..702780b
--- /dev/null
+++ b/.pynchon.json5
@@ -0,0 +1,142 @@
+{ // BEGIN: top-level pynchon config
+ // Everything here is optional.
+ // Literals only; no templating please!
+ "plugins": [
+ // defaults; these are included anyway
+ 'render', 'gen', 'project', 'plugins',
+ 'jinja', 'json', 'git', 'core', 'gripe',
+ 'dockerhub',
+ "makefile", // makefile parsing
+
+ // Recommended for projects with source-code
+ "docs", "src", "test","github",
+ "vhs", // vhs rendering
+
+ // For projects with intermediate representations
+ "dot", // graphviz dot-files
+ "jinja", // projects with templated docs
+ // Docs-Generation
+
+ "pandoc", "mkdocs", "deck",
+
+ // Experimental
+ "griffe", // ast tool
+ "pattern", // replacement for scaffold, probably
+ ],
+ // END: top-level config
+
+ // Config for `docs` plugin:
+ // Runs a webserver to open docs-files
+ "docs": {
+ "root": "{{pynchon.root}}/docs",
+ "include_patterns": [
+ "{{pynchon.root}}/*.md",
+ "{{docs.root}}/**/*.md",
+ "{{docs.root}}/**/*.html",
+ ],
+ "apply_hooks": ["open-after"],
+ },
+ // Config for `dockerhub` plugin:
+ "dockerhub":{
+ "org_name": "robotwranglers",
+ "repo_name": "imgrot",
+ },
+
+ // END: core plugins-config
+ // BEGIN: other plugins-config
+
+ // Config for `deck` plugin:
+ // Renders slide-decks from markdown
+ "deck": {
+ "root": "{{docs.root}}/slides",
+ "apply_hooks": ["open-after"],
+ "pandoc_engine":"dzslides",
+ "pandoc_args": ["--css dzslides.css"],
+ "pandoc_docker": "pandoc/core",
+ },
+
+ // Config for `github` plugin:
+ "github": {},
+
+ // Config for `hooks` plugin:
+ "hooks": {},
+
+
+ // Config for `jinja` plugin:
+ // Planner for finding/rendering project .j2 files
+ "jinja":{
+ "vars": {},
+
+ "filter_includes": [
+ // where to load jinja filters from
+ // allows {tpls|paths|globs}
+ ],
+
+ "exclude_patterns": [
+ // Describes files that shouldn't show up in plans
+ // `globals.exclude_patterns` will be appended.
+ "src/**/*.j2",
+ ],
+
+ "template_includes": [
+ // Paths to load includes from.
+ // (Used in templates as `{% include .. %}`)
+ "{{docs.root}}/includes",
+ ],
+ },
+
+ // Config for `dot` plugin:
+ // Tool for working with dot (aka graphviz)
+ "dot": {
+ "output_format": "png",
+ },
+
+ // Config for `fixme` plugin:
+ // `globals.exclude_patterns` will be appended.
+ "fixme": {
+ "exclude_patterns": [
+ "**/*.egg-info/**",
+ "{{src.root}}/**/fixme/**",
+ "{{src.root}}/**/fixme.py",
+ "{{src.root}}/**/python/api/**",
+ "{{src.root}}/pynchon/annotate.py",
+ ],
+ },
+ // Config for `python-api` plugin:
+ // This generates API docs for python-packages
+ "python":{},
+ // Config for `python-api` plugin:
+ // This generates API docs for python-packages
+ "python-api":{
+ "skip_private_methods": true,
+ "skip_patterns": [],
+ },
+
+ // Config for `scaffolding` plugin:
+ // This provides ways to synchronize/diff project boilerplate.
+ // (Similar to cookie-cutter[], but more simple to use.)
+ "scaffolding":{
+ "exclude_patterns": [
+ // includes globals
+ ],
+ "scaffolds":[
+ // list of Scaffolds-objects
+ {
+ "name": "subproject tox.ini's",
+ "pattern": "**/tox.ini",
+ "scope":"*",
+ "src":"pyproject.toml",
+ },
+ ],
+ },
+
+ // Config for `pypi` plugin:
+ // Provider for details about the PyPI this project uses.
+ // (This is only used in rendering docs; `pynchon` does not manage releases.
+ // You can probably leave this blank for public PyPI but pynchon's own
+ // config has some values just1to ensure the plugin is exercised.)
+ "pypi": {
+ "name":"THE public PyPI"
+ },
+
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3247bdb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,8 @@
+# syntax=docker/dockerfile:1
+FROM debian:bookworm
+WORKDIR /workspace
+RUN apt-get update && apt-get -y install python3-dev python3-pip chafa imagemagick ffmpeg libsm6 libxext6
+RUN mkdir /opt/imgrot
+COPY . /opt/imgrot
+RUN pip3 install -r /opt/imgrot/requirements.txt --break-system-packages
+ENTRYPOINT [ "python3", "/opt/imgrot/imgrot.py" ]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4aa4d93
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,106 @@
+##
+# Docker project makefile.
+##
+.SHELL := bash
+MAKEFLAGS += --warn-undefined-variables
+# .SHELLFLAGS := -euo pipefail -c
+.DEFAULT_GOAL := none
+
+THIS_MAKEFILE := $(abspath $(firstword $(MAKEFILE_LIST)))
+THIS_MAKEFILE := `python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' ${THIS_MAKEFILE}`
+SRC_ROOT := $(shell dirname ${THIS_MAKEFILE})
+
+DOCKER_IMAGE_NAME?=imgrot
+DOCKER_ORG?=robotwranglers
+SHORT_SHA=$(shell git rev-parse --short HEAD)
+PYNCHON_CLI_VERSION=baf56b7
+# pynchon.run=docker run -v `pwd`:/workspace -w/workspace robotwranglers/pynchon:${PYNCHON_CLI_VERSION}
+pynchon.run=pynchon
+
+.PHONY: build docs
+
+init:
+build: docker.build
+clean: docker.clean py-clean
+
+docs: docs.vhs docs.jinja docs.rotations
+ python imgrot.py img/icon.png --stream > img/demo.gif
+docs.jinja:
+ ${pynchon.run} jinja render README.md.j2
+docs.vhs:; PS1="$$ " sh -c "${pynchon.run} vhs apply"
+docs.rotations:
+ 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:
+ docker rmi $(DOCKER_IMAGE_NAME) >/dev/null || true
+
+docker.build build.docker:
+ docker build -t $(DOCKER_IMAGE_NAME) .
+ docker tag $(DOCKER_IMAGE_NAME) ${DOCKER_ORG}/imgrot:latest
+ docker tag $(DOCKER_IMAGE_NAME) ${DOCKER_ORG}/imgrot:${SHORT_SHA}
+
+docker.push:
+ docker push ${DOCKER_ORG}/imgrot:latest
+ docker push ${DOCKER_ORG}/imgrot:${SHORT_SHA}
+
+docker.shell:
+ docker run -it --rm -v `pwd`:/workspace -w /workspace \
+ --entrypoint bash $(DOCKER_IMAGE_NAME)
+
+docker.base=docker run --rm -v `pwd`:/workspace -w /workspace
+docker.test:
+ set -x \
+ && ${docker.base} \
+ --entrypoint sh $(DOCKER_IMAGE_NAME) \
+ -x -c "ls /opt/imgrot > /dev/null" \
+ && ${docker.base} $(DOCKER_IMAGE_NAME) \
+ img/icon.png \
+ --range 360 --img-shape 200x200 \
+ --stream > .tmp.output.gif \
+ && (which imgcat && .tmp.output.gif || true) \
+ && ${docker.base} $(DOCKER_IMAGE_NAME) \
+ img/icon.png --display
+
+test: docker.test
+
+py-clean:
+ rm -rf tmp.pypi* dist/* build/* \
+ && rm -rf src/*.egg-info/
+ find . -name '*.tmp.*' -delete
+ find . -name '*.pyc' -delete
+ find . -name __pycache__ -delete
+ find . -type d -name .tox | xargs -n1 -I% bash -x -c "rm -rf %"
+ rmdir build || true
+# version:
+# @python setup.py --version
+# pypi-release:
+# PYPI_RELEASE=1 make build \
+# && twine upload \
+# --user $${PYPI_USER} \
+# --password $${PYPI_TOKEN} \
+# dist/*
+
+release: clean normalize static-analysis test
+
+tox-%:
+ tox -e ${*}
+
+normalize: tox-normalize
+lint static-analysis: tox-static-analysis
+# smoke-test stest: tox-stest
+# test-integrations itest: tox-itest
+# utest test-units: tox-utest
+# dtest: tox-dtest
+# docs-test: dtest
+# test: test-units test-integrations smoke-test
+# iterate: clean normalize lint test
\ No newline at end of file
diff --git a/README.md b/README.md
index a71802b..1597b74 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,161 @@
-# Perspective Transformation along specific axes
+
-
-Rotate along X axis and translate 5 pixel along X axis
+## Overview
-
-Rotate along XZ axis
+A fork / update for the excellent original work at [eborboihuc/rotate_3d](https://github.com/eborboihuc/rotate_3d).
-## Prerequisites
+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.
-- Linux
-- Python 2.7 with numpy
-- OpenCV 2.4.9
+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)
+
+-------------------------------------
+
+## Installation
+
+Nothing on pypi. See [dockerhub](https://hub.docker.com/r/robotwranglers/imgrot) for available releases.
+
+```
+pip install -r requirements.txt
+```
+
+-------------------------------------
## Usage
-Change main function with ideal [arguments](#parameters)
+**Basic usage info follows:**
```bash
-python demo.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
-```python
-- rotated_img = it.rotate_along_axis(phi = ang, dx = 5)
-+ #rotated_img = it.rotate_along_axis(phi = ang, dx = 5)
-
-- #rotated_img = it.rotate_along_axis(phi = ang, gamma = ang)
-+ rotated_img = it.rotate_along_axis(phi = ang, gamma = ang)
+Usage: imgrot.py [OPTIONS] IMG_PATH
+
+Options:
+ --bg TEXT Background color to pass to chafa
+ --display Display output with chafa
+ --invert Pass --invert to chafa
+ --img-shape TEXT Ideal image shape in WxH format (optional)
+ --duration TEXT Duration argument to pass to chafa
+ --output-dir TEXT Output directory for frames
+ --output-file TEXT Output file for animated gif
+ --range TEXT Range to rotate through
+ --stream / --no-stream Stream output in raw format (for use with pipes).
+ Implies --animate
+ --verbose Whether or not ffmpeg-stderr is displayed
+ --view View a file with chafa (generates nothing)
+ --rotation TEXT One of { x | y | yz }
+ --speed TEXT Speed factor to pass to ffmpeg (default=.08)
+ --stretch Whether to pass --stretch to chafa
+ --help Show this message and exit.
+
```
-Then
+
+You can also set `LOGLEVEL=debug` for more info.
+
+-------------------------------------
+
+## Usage from Docker
+
+A few examples of usage from docker:
+
+#### Saving an Animation
+
```bash
-python demo.py images/000001.jpg 360
+$ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --range 360 --img-shape 200x200 --stream > demo.gif
```
-## Parameters:
+
+
+
+
+#### Terminal-Friendly Display
-```python
-it = ImageTransformer(img_path, img_shape)
-it.rotate_along_axis(theta=0, phi=0, gamma=0, dx=0, dy=0, dz=0):
+```bash
+$ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --display --stretch --bg lightblue
```
-- img_path : the path of image that you want rotated
-- shape : the ideal shape of input image, None for original size.
-- phi : the rotation around the y axis
-- gamma : the rotation around the z axis (basically a 2D rotation)
-- dx : translation along the x axis
-- dy : translation along the y axis
-- dz : translation along the z axis (distance to the image)
+
+
+
+
+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.
-## Acknowledgments
+------------------------------
-Code ported and modified from [jepson](http://jepsonsblog.blogspot.tw/2012/11/rotation-in-3d-using-opencvs.html) and [stackoverflow](http://stackoverflow.com/questions/17087446/how-to-calculate-perspective-transform-for-opencv-from-rotation-angles). Thanks for their excellent work!
+#### Changing Axis of Rotation
-## Author
+The rotation can be controlled to create a bunch of different effects:
+
+```bash
+$ docker run -it --rm -v `pwd`:/workspace \
+ -w /workspace robotwranglers/imgrot \
+ img/icon.png \
+ --display --stretch \
+ --bg darkgreen \
+ --rotation
+```
-Hou-Ning Hu / [@eborboihuc](https://eborboihuc.github.io/)
+
+
+
+ | x |
+ y |
+ s,swivel |
+
+
+
+  |
+
+
+ |
+  |
+
+
+ | j,jitter |
+ w,wobble |
+ f,flip |
+
+
+
+  |
+
+
+ |
+  |
+
+
+ | exit-ul |
+ exit-ur |
+ exit-lr |
+
+
+
+  |
+
+
+ |
+  |
+
+
+
diff --git a/README.md.j2 b/README.md.j2
new file mode 100644
index 0000000..458afbb
--- /dev/null
+++ b/README.md.j2
@@ -0,0 +1,141 @@
+
+
+## Overview
+
+A fork / update for the excellent original work at [eborboihuc/rotate_3d](https://github.com/eborboihuc/rotate_3d).
+
+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.
+
+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)
+
+-------------------------------------
+
+## Installation
+
+Nothing on pypi. See [dockerhub]({{dockerhub.repo_url}}) for available releases.
+
+```
+pip install -r requirements.txt
+```
+
+-------------------------------------
+
+## Usage
+
+**Basic usage info follows:**
+
+```bash
+{{bash('python imgrot.py --help')}}
+```
+
+You can also set `LOGLEVEL=debug` for more info.
+
+-------------------------------------
+
+## Usage from Docker
+
+A few examples of usage from docker:
+
+#### 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
+```
+
+
+
+
+
+#### Terminal-Friendly Display
+
+```bash
+$ docker run -it --rm -v `pwd`:/workspace -w /workspace robotwranglers/imgrot img/icon.png --display --stretch --bg lightblue
+```
+
+
+
+
+
+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
+
+The rotation can be controlled to create a bunch of different effects:
+
+```bash
+$ docker run -it --rm -v `pwd`:/workspace \
+ -w /workspace robotwranglers/imgrot \
+ img/icon.png \
+ --display --stretch \
+ --bg darkgreen \
+ --rotation
+```
+
+
+
+
+ | x |
+ y |
+ s,swivel |
+
+
+
+  |
+
+
+ |
+  |
+
+
+ | j,jitter |
+ w,wobble |
+ f,flip |
+
+
+
+  |
+
+
+ |
+  |
+
+
+ | exit-ul |
+ exit-ur |
+ exit-lr |
+
+
+
+  |
+
+
+ |
+  |
+
+
+
diff --git a/demo.py b/demo.py
deleted file mode 100644
index 011eca5..0000000
--- a/demo.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from image_transformer import ImageTransformer
-from util import save_image
-import sys
-import os
-
-# Usage:
-# Change main function with ideal arguments
-# then
-# python demo.py [name of the image] [degree to rotate] ([ideal width] [ideal height])
-# e.g.,
-# python demo.py images/000001.jpg 360
-# python demo.py images/000001.jpg 45 500 700
-#
-# Parameters:
-# img_path : the path of image that you want rotated
-# shape : the ideal shape of input image, None for original size.
-# theta : the rotation around the x axis
-# phi : the rotation around the y axis
-# gamma : the rotation around the z axis (basically a 2D rotation)
-# dx : translation along the x axis
-# dy : translation along the y axis
-# dz : translation along the z axis (distance to the image)
-#
-# Output:
-# image : the rotated image
-
-
-# Input image path
-img_path = sys.argv[1]
-
-# Rotation range
-rot_range = 360 if len(sys.argv) <= 2 else int(sys.argv[2])
-
-# Ideal image shape (w, h)
-img_shape = None if len(sys.argv) <= 4 else (int(sys.argv[3]), int(sys.argv[4]))
-
-# Instantiate the class
-it = ImageTransformer(img_path, img_shape)
-
-# Make output dir
-if not os.path.isdir('output'):
- os.mkdir('output')
-
-# Iterate through rotation range
-for ang in xrange(0, rot_range):
-
- # NOTE: Here we can change which angle, axis, shift
-
- """ Example of rotating an image along y-axis from 0 to 360 degree
- with a 5 pixel shift in +X direction """
- rotated_img = it.rotate_along_axis(phi = ang, dx = 5)
-
- """ Example of rotating an image along yz-axis from 0 to 360 degree """
- #rotated_img = it.rotate_along_axis(phi = ang, gamma = ang)
-
- """ Example of rotating an image along z-axis(Normal 2D) from 0 to 360 degree """
- #rotated_img = it.rotate_along_axis(gamma = ang)
-
- save_image('output/{}.jpg'.format(str(ang).zfill(3)), rotated_img)
-
-
diff --git a/docs/README.original.md b/docs/README.original.md
new file mode 100644
index 0000000..731cb7f
--- /dev/null
+++ b/docs/README.original.md
@@ -0,0 +1,60 @@
+# Perspective Transformation along specific axes
+
+## Animation
+
+
+Rotate along X axis and translate 5 pixel along X axis
+
+
+Rotate along XZ axis
+
+## Prerequisites
+
+- Linux
+- Python 2.7 with numpy
+- OpenCV 2.4.9
+
+## Usage
+
+Change main function with ideal [arguments](#parameters)
+
+```bash
+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
+```python
+- rotated_img = it.rotate_along_axis(phi = ang, dx = 5)
++ #rotated_img = it.rotate_along_axis(phi = ang, dx = 5)
+
+- #rotated_img = it.rotate_along_axis(phi = ang, gamma = ang)
++ rotated_img = it.rotate_along_axis(phi = ang, gamma = ang)
+```
+Then
+```bash
+python imgrot.py img/000001.jpg 360
+```
+
+## Parameters:
+
+```
+it = ImageTransformer(img_path, img_shape)
+it.rotate_along_axis(theta=0, phi=0, gamma=0, dx=0, dy=0, dz=0):
+```
+
+- img_path : the path of image that you want rotated
+- shape : the ideal shape of input image, None for original size.
+- phi : the rotation around the y axis
+- gamma : the rotation around the z axis (basically a 2D rotation)
+- dx : translation along the x axis
+- dy : translation along the y axis
+- dz : translation along the z axis (distance to the image)
+
+
+## Acknowledgments
+
+Code ported and modified from [jepson](http://jepsonsblog.blogspot.tw/2012/11/rotation-in-3d-using-opencvs.html) and [stackoverflow](http://stackoverflow.com/questions/17087446/how-to-calculate-perspective-transform-for-opencv-from-rotation-angles). Thanks for their excellent work!
+
+## Author
+
+Hou-Ning Hu / [@eborboihuc](https://eborboihuc.github.io/)
diff --git a/docs/tape/demo.tape b/docs/tape/demo.tape
new file mode 100644
index 0000000..f5629d3
--- /dev/null
+++ b/docs/tape/demo.tape
@@ -0,0 +1,16 @@
+# This file describes terminal videos with the `vhs` tool
+# See https://github.com/charmbracelet/vhs for docs
+Output img/demo.chafa.gif
+Require "docker"
+Set Shell "sh"
+Set FontSize 16
+Set Width 800
+Set Height 800
+Set TypingSpeed .05
+Set PlaybackSpeed 1
+Set CursorBlink false
+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 2c611bb..bc154ee 100644
--- a/image_transformer.py
+++ b/image_transformer.py
@@ -1,8 +1,4 @@
-from util import *
-import numpy as np
-import cv2
-
-# Usage:
+# Usage:
# Change main function with ideal arguments
# Then
# from image_tranformer import ImageTransformer
@@ -19,31 +15,136 @@
#
# Output:
# image : the rotated image
-#
+#
# Reference:
# 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))
+
+
+def get_deg(rtheta, rphi, rgamma):
+ return (rad_to_deg(rtheta), rad_to_deg(rphi), rad_to_deg(rgamma))
+
+
+def deg_to_rad(deg):
+ return deg * pi / 180.0
-class ImageTransformer(object):
- """ Perspective transformation class for image
- with shape (height, width, #channels) """
+
+def rad_to_deg(rad):
+ return rad * 180.0 / pi
+
+
+class ImageTransformer:
+ """
+ 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 = load_image(image_path, shape)
-
+ self.image = self.load_image(image_path, shape)
+
self.height = self.image.shape[0]
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)
- """ Wrapper of Rotating a Image """
def rotate_along_axis(self, theta=0, phi=0, gamma=0, dx=0, dy=0, dz=0):
-
+ """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)
-
+
# Get ideal focal length on z axis
# NOTE: Change this section to other axis if needed
d = np.sqrt(self.height**2 + self.width**2)
@@ -52,53 +153,53 @@ 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
# Projection 2D -> 3D matrix
- A1 = np.array([ [1, 0, -w/2],
- [0, 1, -h/2],
- [0, 0, 1],
- [0, 0, 1]])
-
+ A1 = np.array([[1, 0, -w / 2], [0, 1, -h / 2], [0, 0, 1], [0, 0, 1]])
+
# Rotation matrices around the X, Y, and Z axis
- RX = np.array([ [1, 0, 0, 0],
- [0, np.cos(theta), -np.sin(theta), 0],
- [0, np.sin(theta), np.cos(theta), 0],
- [0, 0, 0, 1]])
-
- RY = np.array([ [np.cos(phi), 0, -np.sin(phi), 0],
- [0, 1, 0, 0],
- [np.sin(phi), 0, np.cos(phi), 0],
- [0, 0, 0, 1]])
-
- RZ = np.array([ [np.cos(gamma), -np.sin(gamma), 0, 0],
- [np.sin(gamma), np.cos(gamma), 0, 0],
- [0, 0, 1, 0],
- [0, 0, 0, 1]])
+ RX = np.array(
+ [
+ [1, 0, 0, 0],
+ [0, np.cos(theta), -np.sin(theta), 0],
+ [0, np.sin(theta), np.cos(theta), 0],
+ [0, 0, 0, 1],
+ ]
+ )
+
+ RY = np.array(
+ [
+ [np.cos(phi), 0, -np.sin(phi), 0],
+ [0, 1, 0, 0],
+ [np.sin(phi), 0, np.cos(phi), 0],
+ [0, 0, 0, 1],
+ ]
+ )
+
+ RZ = np.array(
+ [
+ [np.cos(gamma), -np.sin(gamma), 0, 0],
+ [np.sin(gamma), np.cos(gamma), 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 1],
+ ]
+ )
# Composed rotation matrix with (RX, RY, RZ)
R = np.dot(np.dot(RX, RY), RZ)
# Translation matrix
- T = np.array([ [1, 0, 0, dx],
- [0, 1, 0, dy],
- [0, 0, 1, dz],
- [0, 0, 0, 1]])
+ T = np.array([[1, 0, 0, dx], [0, 1, 0, dy], [0, 0, 1, dz], [0, 0, 0, 1]])
# Projection 3D -> 2D matrix
- A2 = np.array([ [f, 0, w/2, 0],
- [0, f, h/2, 0],
- [0, 0, 1, 0]])
+ A2 = np.array([[f, 0, w / 2, 0], [0, f, h / 2, 0], [0, 0, 1, 0]])
# Final transformation matrix
return np.dot(A2, np.dot(T, np.dot(R, A1)))
-
diff --git a/images/000001.jpg b/images/000001.jpg
deleted file mode 100755
index 5a37d29..0000000
Binary files a/images/000001.jpg and /dev/null differ
diff --git a/img/demo.chafa.gif b/img/demo.chafa.gif
new file mode 100644
index 0000000..1b3c1bf
Binary files /dev/null and b/img/demo.chafa.gif differ
diff --git a/img/demo.gif b/img/demo.gif
new file mode 100644
index 0000000..388a831
Binary files /dev/null and b/img/demo.gif differ
diff --git a/img/graph.png b/img/graph.png
new file mode 100644
index 0000000..e0a084c
Binary files /dev/null and b/img/graph.png differ
diff --git a/img/icon.png b/img/icon.png
new file mode 100644
index 0000000..7b744ec
Binary files /dev/null and b/img/icon.png differ
diff --git a/img/rf.gif b/img/rf.gif
new file mode 100644
index 0000000..4032eab
Binary files /dev/null and b/img/rf.gif differ
diff --git a/img/rj.gif b/img/rj.gif
new file mode 100644
index 0000000..a9d87d8
Binary files /dev/null and b/img/rj.gif differ
diff --git a/img/rll.gif b/img/rll.gif
new file mode 100644
index 0000000..31fbeb2
Binary files /dev/null and b/img/rll.gif differ
diff --git a/img/rlr.gif b/img/rlr.gif
new file mode 100644
index 0000000..c85398f
Binary files /dev/null and b/img/rlr.gif differ
diff --git a/img/rs.gif b/img/rs.gif
new file mode 100644
index 0000000..e48d5dd
Binary files /dev/null and b/img/rs.gif differ
diff --git a/img/rul.gif b/img/rul.gif
new file mode 100644
index 0000000..e4740b3
Binary files /dev/null and b/img/rul.gif differ
diff --git a/img/rur.gif b/img/rur.gif
new file mode 100644
index 0000000..01876f9
Binary files /dev/null and b/img/rur.gif differ
diff --git a/img/rw.gif b/img/rw.gif
new file mode 100644
index 0000000..dab30aa
Binary files /dev/null and b/img/rw.gif differ
diff --git a/img/rx.gif b/img/rx.gif
new file mode 100644
index 0000000..14351bf
Binary files /dev/null and b/img/rx.gif differ
diff --git a/img/ry.gif b/img/ry.gif
new file mode 100644
index 0000000..1660a7a
Binary files /dev/null and b/img/ry.gif differ
diff --git a/imgrot.py b/imgrot.py
new file mode 100644
index 0000000..9f56ed2
--- /dev/null
+++ b/imgrot.py
@@ -0,0 +1,168 @@
+"""
+# Usage:
+# Change main function with ideal arguments
+# then
+# python imgrot.py [name of the image] [degree to rotate] ([ideal width] [ideal height])
+# e.g.,
+# 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
+# shape : ideal shape of input image, None for original size.
+# theta : rotation around the x axis
+# phi : rotation around the y axis
+# gamma : rotation around the z axis (basically a 2D rotation)
+# dx : translation along the x axis
+# dy : translation along the y axis
+# dz : translation along the z axis (distance to the image)
+"""
+
+import os
+import sys
+import logging
+import subprocess
+
+import click
+
+from image_transformer import ImageTransformer
+
+# 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(
+ "--display", is_flag=True, default=False, help="Display output with chafa"
+)
+@click.option("--invert", is_flag=True, default=False, help="Pass --invert to chafa")
+@click.option(
+ "--img-shape", default=None, help="Ideal image shape in WxH format (optional)"
+)
+@click.option(
+ "--duration",
+ default="",
+ help="Duration argument to pass to chafa",
+)
+@click.option("--output-dir", default="/tmp/imgrot", help="Output directory for frames")
+@click.option(
+ "--output-file", default="/tmp/imgrot.gif", help="Output file for animated gif"
+)
+@click.option("--range", "rot_range", default="360", help="Range to rotate through")
+@click.option(
+ "--stream/--no-stream",
+ default=False,
+ help="Stream output in raw format (for use with pipes). Implies --animate",
+)
+@click.option(
+ "--verbose",
+ is_flag=True,
+ default=False,
+ help="Whether or not ffmpeg-stderr is displayed",
+)
+@click.option(
+ "--view",
+ is_flag=True,
+ default=False,
+ help="View a file with chafa (generates nothing)",
+)
+@click.option("--rotation", default="y", help="One of { x | y | yz }")
+@click.option(
+ "--speed", default=".08", help="Speed factor to pass to ffmpeg (default=.08)"
+)
+@click.option(
+ "--stretch",
+ is_flag=True,
+ default=False,
+ help="Whether to pass --stretch to chafa",
+)
+@click.argument("img_path", required=True)
+def run(
+ img_path: str,
+ stretch: bool = False,
+ duration: str = "",
+ display: bool = False,
+ invert: bool = False,
+ verbose: bool = False,
+ img_shape=None,
+ speed: str = "0.08",
+ rotation: str = "y",
+ bg: str = "black",
+ output_dir: str = "/tmp",
+ output_file: str = "/tmp/imgrot.gif",
+ rot_range: str = "360",
+ stream: bool = False,
+ view: bool = False,
+):
+ stretch = "--stretch" if stretch else ""
+ duration = f"--duration {duration}" if duration else ""
+ invert = invert and "--invert" or ""
+ bg = f"--bg {bg}"
+
+ if not os.path.exists(img_path):
+ logger.debug(f"{img_path} does not exist!")
+ raise SystemExit(1)
+
+ 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} .. ")
+ for ang in range(0, rot_range):
+ 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")
+
+ command = f"ls {output_dir}/|wc -l"
+ result = subprocess.run(command, shell=True, capture_output=True, text=True)
+ if result.returncode != 0:
+ logger.debug(f"Command failed with return code {result.returncode}\n")
+ raise SystemExit(result.returncode)
+ else:
+ logger.debug(f"Total Frames: {result.stdout.strip()}")
+ logger.debug("Animating..")
+ quiet_maybe = "" if verbose else "2> /dev/null"
+ commands = [
+ f"ffmpeg -y -i {output_dir}/000.png -vf palettegen=reserve_transparent=1 /tmp/palette.png {quiet_maybe}",
+ f"ffmpeg -y -framerate 140 -pattern_type glob -i '{output_dir}/*.png' -i /tmp/palette.png -lavfi paletteuse=alpha_threshold=128 -gifflags -offsetting /tmp/.tmp.gif {quiet_maybe}",
+ 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.debug(cmd)
+ result = subprocess.run(cmd, shell=True, stdout=sys.stderr, stderr=sys.stderr)
+ if result.returncode != 0:
+ logger.critical(f"Command failed with return code {result.returncode}")
+ raise SystemExit(result.returncode)
+ if view:
+ logger.debug(f"Viewing {img_path}")
+ os.system(f"chafa {stretch} {duration} {img_path}")
+ elif display:
+ logger.debug("Displaying animated gif..")
+ os.system(
+ f"chafa {bg} {duration} {stretch} {invert} --symbols 'braille' {output_file}"
+ )
+ elif stream:
+ logger.debug("Streaming animation..")
+ os.system(
+ f"convert {output_file} -fuzz 10% -transparent white {output_file} {quiet_maybe}"
+ )
+ with open(output_file, "rb") as binary_file:
+ content = binary_file.read()
+ sys.stdout.buffer.write(content)
+ else:
+ logger.critical("No instructions, not sure what to do")
+ raise SystemExit(1)
+
+
+if __name__ == "__main__":
+ run()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..7337b47
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+opencv-python
+click
\ No newline at end of file
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..a0dca94
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,99 @@
+##
+# tox automation for ssm
+# https://tox.wiki/en/latest/config.html
+##
+[tox]
+skipsdist = True
+usedevelop = True
+recreate = False
+
+[testenv:ssort]
+description=Sorts the source code
+setenv =
+ {[testenv]setenv}
+deps =
+ ssort==0.11.6
+commands =
+ bash -x -c "env|grep TOX && ssort {toxinidir}/*.py"
+
+[testenv]
+allowlist_externals =
+ bash
+ pytest
+ ipython
+deps =
+ -e .[testing]
+install_command=
+ python -m pip install {packages}
+setenv =
+ PYNCHON_TOX_RUNTIME=True
+
+[testenv:shell]
+description=Debugging shell for this project
+commands =
+ python -m ssm shell
+deps =
+ IPython
+
+# [testenv:type-check]
+# description=
+# Type checking for this project. Informational; this is not enforced yet
+# recreate = False
+# env_dir={toxinidir}/../.tox/{env_name}
+# deps=
+# -e .[typing]
+# commands =
+# bash -x -c "\
+# (mypy --install-types --non-interactive src/||true)"
+
+# [testenv:utest]
+# description=Unit tests
+# setenv =
+# {[testenv]setenv}
+# TEST_SUITE=units
+# commands =
+# bash -x -c "env && pytest -s tests/units"
+
+# [testenv:stest]
+# description=Smoke tests
+# setenv =
+# {[testenv]setenv}
+# TEST_SUITE=smoke
+# commands =
+# bash -x -c "env && bash -x tests/smoke/test.sh && pytest -s tests/smoke"
+
+# [testenv:itest]
+# description=Integration tests
+# setenv =
+# {[testenv]setenv}
+# TEST_SUITE=integration
+# commands =
+# bash -x -c "env && bash -x tests/integration/test.sh && pytest --exitfirst -s tests/integration"
+
+[testenv:normalize]
+description = Normalizes code for this project
+deps =
+ shed==2023.5.1
+ autopep8==1.7.0
+ isort==5.11.5
+commands =
+ bash -x -c "\
+ shed; \
+ autopep8 --recursive --in-place {toxinidir}/*.py; \
+ isort --settings-file {toxinidir}/.isort.cfg {toxinidir}/*.py;"
+ # bash -x -c "\
+ # autopep8 --recursive --in-place {toxinidir}/tests; \
+ # isort --settings-file {toxinidir}/.isort.cfg {toxinidir}/tests;"
+
+[testenv:static-analysis]
+description = Runs Flake8
+skip_install = True
+recreate = False
+deps =
+ flake8==5.0.4
+ vulture==2.6
+commands =
+ bash -x -c "\
+ flake8 --config {toxinidir}/.flake8 *.py \
+ && vulture {toxinidir}/*.py --min-confidence 90 \
+ "
diff --git a/util.py b/util.py
deleted file mode 100644
index 9be24bf..0000000
--- a/util.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from math import pi
-import cv2
-
-""" Utility Functions """
-
-def load_image(img_path, shape=None):
- img = cv2.imread(img_path)
- if shape is not None:
- img = cv2.resize(img, shape)
-
- return img
-
-def save_image(img_path, img):
- cv2.imwrite(img_path, img)
-
-def get_rad(theta, phi, gamma):
- return (deg_to_rad(theta),
- deg_to_rad(phi),
- deg_to_rad(gamma))
-
-def get_deg(rtheta, rphi, rgamma):
- return (rad_to_deg(rtheta),
- rad_to_deg(rphi),
- rad_to_deg(rgamma))
-
-def deg_to_rad(deg):
- return deg * pi / 180.0
-
-def rad_to_deg(rad):
- return deg * 180.0 / pi