diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..4c9a743 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,60 @@ +version: '{branch}-{build}' +build: off +environment: + global: + COVERALLS_EXTRAS: '-v' + COVERALLS_REPO_TOKEN: CmhNs4cpmc7aNcp5FPB4sf379OiXLhufo + matrix: + - TOXENV: check + TOXPYTHON: C:\Python36\python.exe + PYTHON_HOME: C:\Python36 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '32' + - TOXENV: py36,codecov,coveralls + TOXPYTHON: C:\Python36\python.exe + PYTHON_HOME: C:\Python36 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '32' + - TOXENV: py36,codecov,coveralls + TOXPYTHON: C:\Python36-x64\python.exe + PYTHON_HOME: C:\Python36-x64 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '64' + - TOXENV: py37,codecov,coveralls + TOXPYTHON: C:\Python37\python.exe + PYTHON_HOME: C:\Python37 + PYTHON_VERSION: '3.7' + PYTHON_ARCH: '32' + - TOXENV: py37,codecov,coveralls + TOXPYTHON: C:\Python37-x64\python.exe + PYTHON_HOME: C:\Python37-x64 + PYTHON_VERSION: '3.7' + PYTHON_ARCH: '64' + - TOXENV: py38,codecov,coveralls + TOXPYTHON: C:\Python38\python.exe + PYTHON_HOME: C:\Python38 + PYTHON_VERSION: '3.8' + PYTHON_ARCH: '32' + - TOXENV: py38,codecov,coveralls + TOXPYTHON: C:\Python38-x64\python.exe + PYTHON_HOME: C:\Python38-x64 + PYTHON_VERSION: '3.8' + PYTHON_ARCH: '64' +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' + - '%PYTHON_HOME%\Scripts\virtualenv --version' + - '%PYTHON_HOME%\Scripts\easy_install --version' + - '%PYTHON_HOME%\Scripts\pip --version' + - '%PYTHON_HOME%\Scripts\tox --version' +test_script: + - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..913ac15 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,21 @@ +[bumpversion] +current_version = 0.4.2 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:README.rst] +search = v{current_version}. +replace = v{new_version}. + +[bumpversion:file:docs/conf.py] +search = version = release = '{current_version}' +replace = version = release = '{new_version}' + +[bumpversion:file:src/python_eulerian_video_magnification/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + diff --git a/.cookiecutterrc b/.cookiecutterrc new file mode 100644 index 0000000..4334344 --- /dev/null +++ b/.cookiecutterrc @@ -0,0 +1,69 @@ +# This file exists so you can easily regenerate your project. +# +# `cookiepatcher` is a convenient shim around `cookiecutter` +# for regenerating projects (it will generate a .cookiecutterrc +# automatically for any template). To use it: +# +# pip install cookiepatcher +# cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path +# +# See: +# https://pypi.org/project/cookiepatcher +# +# Alternatively, you can run: +# +# cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary + +default_context: + + _extensions: ['jinja2_time.TimeExtension'] + _template: 'gh:ionelmc/cookiecutter-pylibrary' + allow_tests_inside_package: 'no' + appveyor: 'yes' + c_extension_function: 'longest' + c_extension_module: '_python_eulerian_video_magnification' + c_extension_optional: 'no' + c_extension_support: 'no' + c_extension_test_pypi: 'no' + c_extension_test_pypi_username: 'vgoehler' + codacy: 'no' + codacy_projectid: '[Get ID from https://app.codacy.com/app/vgoehler/PyEVM/settings]' + codeclimate: 'no' + codecov: 'yes' + command_line_interface: 'argparse' + command_line_interface_bin_name: 'EVM' + coveralls: 'yes' + coveralls_token: 'CmhNs4cpmc7aNcp5FPB4sf379OiXLhufo' + distribution_name: 'PyEVM' + email: 'volker.goehler@informatik.tu-freiberg.de' + full_name: 'Volker G Göhler' + landscape: 'no' + license: 'BSD 2-Clause License' + linter: 'flake8' + package_name: 'python_eulerian_video_magnification' + project_name: 'Python Eulerian Video Magnification' + project_short_description: 'Eulerian Video Magnification for Python' + pypi_badge: 'no' + pypi_disable_upload: 'yes' + release_date: 'today' + repo_hosting: 'github.com' + repo_hosting_domain: 'github.com' + repo_name: 'PyEVM' + repo_username: 'vgoehler' + requiresio: 'yes' + scrutinizer: 'no' + setup_py_uses_setuptools_scm: 'no' + setup_py_uses_test_runner: 'no' + sphinx_docs: 'yes' + sphinx_docs_hosting: 'https://PyEVM.readthedocs.io/' + sphinx_doctest: 'no' + sphinx_theme: 'sphinx-rtd-theme' + test_matrix_configurator: 'no' + test_matrix_separate_coverage: 'no' + test_runner: 'pytest' + travis: 'yes' + travis_osx: 'no' + version: '0.1.0' + website: 'https://github.com/vgoehler' + year_from: '2019' + year_to: '2020' diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..7a0bb1c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,16 @@ +[paths] +source = + src + */site-packages + +[run] +branch = true +source = + python_eulerian_video_magnification + tests +parallel = true + +[report] +show_missing = true +precision = 2 +omit = *migrations* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6eb7567 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# see https://editorconfig.org/ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 + +[*.{bat,cmd,ps1}] +end_of_line = crlf diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml new file mode 100644 index 0000000..5639973 --- /dev/null +++ b/.github/workflows/pythonapp.yml @@ -0,0 +1,30 @@ +name: Python application + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pip install pytest + pytest diff --git a/.gitignore b/.gitignore index 6f5f4b8..a98fcac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,72 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ *.py[cod] -*$py.class +__pycache__ # C extensions *.so -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg +# Packages *.egg - -# 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 +*.egg-info +dist +build +eggs +.eggs +parts +bin +var +sdist +wheelhouse +develop-eggs +.installed.cfg +lib +lib64 +venv*/ +pyvenv*/ +pip-wheel-metadata/ # Installer logs pip-log.txt -pip-delete-this-directory.txt # Unit test / coverage reports -htmlcov/ -.tox/ .coverage +.tox .coverage.* -.cache +.pytest_cache/ nosetests.xml coverage.xml -*,cover -.hypothesis/ +htmlcov # Translations *.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv +# Mr Developer +.mr.developer.cfg +.project +.pydevproject +.idea +*.iml +*.komodoproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build + +.DS_Store +*~ +.*.sw[po] +.build +.ve .env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject -.idea/ +.cache +.pytest +.benchmarks +.bootstrap +.appveyor.token +*.bak + +# Mypy Cache +.mypy_cache/ +videos/ diff --git a/.idea/PyEVM.iml b/.idea/PyEVM.iml deleted file mode 100644 index 6f63a63..0000000 --- a/.idea/PyEVM.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 50f67a9..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b31283..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 8676f1e..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index bc567bf..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 2ea730a..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,668 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - DEFINITION_ORDER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - project - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1474511035381 - - - 1474540220082 - - - 1474540291254 - - - 1474544662679 - - - 1474594348952 - - - 1474595587044 - - - 1474611859915 - - - 1474704746089 - - - 1474704752711 - - - 1474787853257 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..817ea0d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,43 @@ +language: python +dist: xenial +cache: false +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all +matrix: + include: + - python: '3.6' + env: + - TOXENV=check + - python: '3.7' + env: + - TOXENV=docs + - env: + - TOXENV=py36,codecov,coveralls + python: '3.6' + - env: + - TOXENV=py37,codecov,coveralls + python: '3.7' + - env: + - TOXENV=py38,codecov,coveralls + python: '3.8' +before_install: + - python --version + - uname -a + - lsb_release -a || true +install: + - python -mpip install --progress-bar=off tox -rci/requirements.txt + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/1.jpg b/1.jpg deleted file mode 100644 index fcef48a..0000000 Binary files a/1.jpg and /dev/null differ diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..f4881bb --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,10 @@ + +Authors +======= + +* Volker G Göhler - https://github.com/vgoehler + +EVM Project Forked from +----------------------- + +* flyingzhao - https://github.com/flyingzhao/PyEVM diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..dfc1d11 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,8 @@ + +Changelog +========= + +0.1.0 (2020-01-02) +------------------ + +* First release on PyPI. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..265b992 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,90 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +Bug reports +=========== + +When `reporting a bug `_ please include: + + * Your operating system name and version. + * Any details about your local setup that might be helpful in troubleshooting. + * Detailed steps to reproduce the bug. + +Documentation improvements +========================== + +Python Eulerian Video Magnification could always use more documentation, whether as part of the +official Python Eulerian Video Magnification docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Feature requests and feedback +============================= + +The best way to send feedback is to file an issue at https://github.com/vgoehler/PyEVM/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) + +Development +=========== + +To set up `PyEVM` for local development: + +1. Fork `PyEVM `_ + (look for the "Fork" button). +2. Clone your fork locally:: + + git clone git@github.com:vgoehler/PyEVM.git + +3. Create a branch for local development:: + + git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: + + tox + +5. Commit your changes and push your branch to GitHub:: + + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +If you need some code review or feedback while you're developing the code just make the pull request. + +For merging, you should: + +1. Include passing tests (run ``tox``) [1]_. +2. Update documentation when there's new API, functionality etc. +3. Add a note to ``CHANGELOG.rst`` about the changes. +4. Add yourself to ``AUTHORS.rst``. + +.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will + `run the tests `_ for each change you add in the pull request. + + It will be slower though ... + +Tips +---- + +To run a subset of tests:: + + tox -e envname -- pytest -k test_myfeature + +To run all the test environments in *parallel* (you need to ``pip install detox``):: + + detox diff --git a/EVM.py b/EVM.py deleted file mode 100644 index ed89125..0000000 --- a/EVM.py +++ /dev/null @@ -1,166 +0,0 @@ -import cv2 -import numpy as np -import scipy.signal as signal -import scipy.fftpack as fftpack - - -#convert RBG to YIQ -def rgb2ntsc(src): - [rows,cols]=src.shape[:2] - dst=np.zeros((rows,cols,3),dtype=np.float64) - T = np.array([[0.114, 0.587, 0.298], [-0.321, -0.275, 0.596], [0.311, -0.528, 0.212]]) - for i in range(rows): - for j in range(cols): - dst[i, j]=np.dot(T,src[i,j]) - return dst - -#convert YIQ to RBG -def ntsc2rbg(src): - [rows, cols] = src.shape[:2] - dst=np.zeros((rows,cols,3),dtype=np.float64) - T = np.array([[1, -1.108, 1.705], [1, -0.272, -0.647], [1, 0.956, 0.620]]) - for i in range(rows): - for j in range(cols): - dst[i, j]=np.dot(T,src[i,j]) - return dst - -#Build Gaussian Pyramid -def build_gaussian_pyramid(src,level=3): - s=src.copy() - pyramid=[s] - for i in range(level): - s=cv2.pyrDown(s) - pyramid.append(s) - return pyramid - -#Build Laplacian Pyramid -def build_laplacian_pyramid(src,levels=3): - gaussianPyramid = build_gaussian_pyramid(src, levels) - pyramid=[] - for i in range(levels,0,-1): - GE=cv2.pyrUp(gaussianPyramid[i]) - L=cv2.subtract(gaussianPyramid[i-1],GE) - pyramid.append(L) - return pyramid - -#load video from file -def load_video(video_filename): - cap=cv2.VideoCapture(video_filename) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - width, height = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) - fps = int(cap.get(cv2.CAP_PROP_FPS)) - video_tensor=np.zeros((frame_count,height,width,3),dtype='float') - x=0 - while cap.isOpened(): - ret,frame=cap.read() - if ret is True: - video_tensor[x]=frame - x+=1 - else: - break - return video_tensor,fps - -# apply temporal ideal bandpass filter to gaussian video -def temporal_ideal_filter(tensor,low,high,fps,axis=0): - fft=fftpack.fft(tensor,axis=axis) - frequencies = fftpack.fftfreq(tensor.shape[0], d=1.0 / fps) - bound_low = (np.abs(frequencies - low)).argmin() - bound_high = (np.abs(frequencies - high)).argmin() - fft[:bound_low] = 0 - fft[bound_high:-bound_high] = 0 - fft[-bound_low:] = 0 - iff=fftpack.ifft(fft, axis=axis) - return np.abs(iff) - -# build gaussian pyramid for video -def gaussian_video(video_tensor,levels=3): - for i in range(0,video_tensor.shape[0]): - frame=video_tensor[i] - pyr=build_gaussian_pyramid(frame,level=levels) - gaussian_frame=pyr[-1] - if i==0: - vid_data=np.zeros((video_tensor.shape[0],gaussian_frame.shape[0],gaussian_frame.shape[1],3)) - vid_data[i]=gaussian_frame - return vid_data - -#amplify the video -def amplify_video(gaussian_vid,amplification=50): - return gaussian_vid*amplification - -#reconstract video from original video and gaussian video -def reconstract_video(amp_video,origin_video,levels=3): - final_video=np.zeros(origin_video.shape) - for i in range(0,amp_video.shape[0]): - img = amp_video[i] - for x in range(levels): - img=cv2.pyrUp(img) - img=img+origin_video[i] - final_video[i]=img - return final_video - -#save video to files -def save_video(video_tensor): - fourcc = cv2.VideoWriter_fourcc('M','J','P','G') - [height,width]=video_tensor[0].shape[0:2] - writer = cv2.VideoWriter("out.avi", fourcc, 30, (width, height), 1) - for i in range(0,video_tensor.shape[0]): - writer.write(cv2.convertScaleAbs(video_tensor[i])) - writer.release() - -#magnify color -def magnify_color(video_name,low,high,levels=3,amplification=20): - t,f=load_video(video_name) - gau_video=gaussian_video(t,levels=levels) - filtered_tensor=temporal_ideal_filter(gau_video,low,high,f) - amplified_video=amplify_video(filtered_tensor,amplification=amplification) - final=reconstract_video(amplified_video,t,levels=3) - save_video(final) - -#build laplacian pyramid for video -def laplacian_video(video_tensor,levels=3): - tensor_list=[] - for i in range(0,video_tensor.shape[0]): - frame=video_tensor[i] - pyr=build_laplacian_pyramid(frame,levels=levels) - if i==0: - for k in range(levels): - tensor_list.append(np.zeros((video_tensor.shape[0],pyr[k].shape[0],pyr[k].shape[1],3))) - for n in range(levels): - tensor_list[n][i] = pyr[n] - return tensor_list - -#butterworth bandpass filter -def butter_bandpass_filter(data, lowcut, highcut, fs, order=5): - omega = 0.5 * fs - low = lowcut / omega - high = highcut / omega - b, a = signal.butter(order, [low, high], btype='band') - y = signal.lfilter(b, a, data, axis=0) - return y - -#reconstract video from laplacian pyramid -def reconstract_from_tensorlist(filter_tensor_list,levels=3): - final=np.zeros(filter_tensor_list[-1].shape) - for i in range(filter_tensor_list[0].shape[0]): - up = filter_tensor_list[0][i] - for n in range(levels-1): - up=cv2.pyrUp(up)+filter_tensor_list[n + 1][i]#可以改为up=cv2.pyrUp(up) - final[i]=up - return final - -#manify motion -def magnify_motion(video_name,low,high,levels=3,amplification=20): - t,f=load_video(video_name) - lap_video_list=laplacian_video(t,levels=levels) - filter_tensor_list=[] - for i in range(levels): - filter_tensor=butter_bandpass_filter(lap_video_list[i],low,high,f) - filter_tensor*=amplification - filter_tensor_list.append(filter_tensor) - recon=reconstract_from_tensorlist(filter_tensor_list) - final=t+recon - save_video(final) - -if __name__=="__main__": - # magnify_color("baby.mp4",0.4,3) - magnify_motion("baby.mp4",0.4,3) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f1d10d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +BSD 2-Clause License + +Copyright (c) 2019-2020, Volker G Göhler. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..6e304e1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,19 @@ +graft docs +graft src +graft ci +graft tests + +include .bumpversion.cfg +include .coveragerc +include .cookiecutterrc +include .editorconfig + +include AUTHORS.rst +include CHANGELOG.rst +include CONTRIBUTING.rst +include LICENSE +include README.rst + +include tox.ini .travis.yml .appveyor.yml + +global-exclude *.py[cod] __pycache__/* *.so *.dylib diff --git a/README.md b/README.md deleted file mode 100644 index 45d993b..0000000 --- a/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Python implementation of EVM(Eulerian Video Magnification) - -This is a python implementation of eulerian video magnification《[Eulerian Video Magnification for Revealing Subtle Changes in the World](http://people.csail.mit.edu/mrub/evm/)》. ->Our goal is to reveal temporal variations in videos that are difficult or impossible to see with the naked eye and display them in an indicative manner. Our method, which we call Eulerian Video Magnification, takes a standard video sequence as input, and applies spatial decomposition, followed by temporal filtering to the frames. The resulting signal is then amplified to reveal hidden information.Using our method, we are able to visualize the flow of blood as it fills the face and also to amplify and reveal small motions. Our technique can run in real time to show phenomena occurring at temporal frequencies selected by the user. - -## Install OpenCV3 -Since the OpenCV3.X does not support Python3, you need to install opencv3 manually. - -Firstly,download opencv3 for python3: ->OpenCV3 for Python3: http://www.lfd.uci.edu/~gohlke/pythonlibs/#opencv - -Then install opencv3 with pip: -``` -pip install opencv_python-3.1.0-cp35-cp35m-win_amd64.whl -``` - -## Other Libraries -* SciPy for signal processing -* NumPy for image processing - -## Result -Original video: -![原图](http://img.blog.csdn.net/20160927155312178) - -Color magnification: -![色彩放大](http://img.blog.csdn.net/20160927155358125) -The color of chest changes. - -Motion magnification: -![运动放大](http://img.blog.csdn.net/20160927155455071) -You can see the motion of chest has been magnified. - -## Chinese version -You can read my blog for more information ->http://blog.csdn.net/tinyzhao/article/details/52681250 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..623d44d --- /dev/null +++ b/README.rst @@ -0,0 +1,147 @@ +======== +Overview +======== + +.. start-badges + +.. list-table:: + :stub-columns: 1 + + * - docs + - |docs| + * - tests + - | |travis| |appveyor| |requires| + | |coveralls| |codecov| + * - package + - | |commits-since| +.. |docs| image:: https://readthedocs.org/projects/pyevm/badge/?style=flat + :target: https://readthedocs.org/projects/pyevm + :alt: Documentation Status + +.. |travis| image:: https://api.travis-ci.com/vgoehler/PyEVM.svg?branch=master + :alt: Travis-CI Build Status + :target: https://travis-ci.com/vgoehler/PyEVM + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/vgoehler/PyEVM?branch=master&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/vgoehler/PyEVM + +.. |requires| image:: https://requires.io/github/vgoehler/PyEVM/requirements.svg?branch=master + :alt: Requirements Status + :target: https://requires.io/github/vgoehler/PyEVM/requirements/?branch=master + +.. |coveralls| image:: https://coveralls.io/repos/vgoehler/PyEVM/badge.svg?branch=master&service=github + :alt: Coverage Status + :target: https://coveralls.io/github/vgoehler/PyEVM + +.. |codecov| image:: https://codecov.io/gh/vgoehler/PyEVM/branch/master/graphs/badge.svg?branch=master + :alt: Coverage Status + :target: https://codecov.io/github/vgoehler/PyEVM + +.. |commits-since| image:: https://img.shields.io/github/commits-since/vgoehler/PyEVM/v0.4.2.svg + :alt: Commits since latest release + :target: https://github.com/vgoehler/PyEVM/compare/v0.4.2...master + + + +.. end-badges + +Eulerian Video Magnification for Python + +This is a python implementation of Eulerian Video Magnification ([Eulerian Video Magnification for Revealing Subtle Changes in the World](http://people.csail.mit.edu/mrub/evm/)). +>Our goal is to reveal temporal variations in videos that are difficult or impossible to see with the naked eye and display them in an indicative manner. Our method, which we call Eulerian Video Magnification, takes a standard video sequence as input, and applies spatial decomposition, followed by temporal filtering to the frames. The resulting signal is then amplified to reveal hidden information.Using our method, we are able to visualize the flow of blood as it fills the face and also to amplify and reveal small motions. Our technique can run in real time to show phenomena occurring at temporal frequencies selected by the user. + +This is a fork from [flyingzhao/PyEVM](https://github.com/flyingzhao/PyEVM) as a basis for own work. +It now has an operational command line interface and is install able. + + + +* Free software: BSD 2-Clause License + +Installation +============ + +Up until now it is not available with PyPI, but if it will be you could use this code to install it. + +:: + + pip install PyEVM + +You can install the in-development version with:: + + pip install https://github.com/vgoehler/PyEVM/archive/master.zip + +needed libraries (that get automatically installed) are: + +- numpy (>=1.17.4) +- opencv-python (>=4.1.2.30) +- scipy (>=1.3.3) + + +Running +======= + +Navigate to sources directory and use + +:: + + python3 -mpython_eulerian_video_magnification inputfile.video + +if you just want to execute the code. + +Usage +===== + +optional arguments: + ================================================ ==================================================== + ``-h, --help`` show this help message and exit + ================================================ ==================================================== + +system arguments: + ================================================ ==================================================== + ``input`` the input video file to work on + ``-o [O]`` output-folder + ``--color_suffix [COLOR_SUFFIX]`` the suffix to use for color modified result files + ``--motion_suffix [MOTION_SUFFIX]`` the suffix to use for motion modified result files + ``--log {debug,info,warning,error,critical}`` log level + ================================================ ==================================================== + +parameters: + =================================================== ==================================================== + ``-m {color,motion}`` mode + ``-c LOW, --low LOW`` low parameter (creek) + ``-p HIGH, --high HIGH`` high parameter (peek) + ``-l LEVELS, --levels LEVELS`` levels parameter + ``-a AMPLIFICATION, --amplification AMPLIFICATION`` amplification parameter + =================================================== ==================================================== + +Documentation +============= + + +https://PyEVM.readthedocs.io/ + + +Development +=========== + +To run all tests run:: + + tox + +Note, to combine the coverage data from all the tox environments run: + +.. list-table:: + :widths: 10 90 + :stub-columns: 1 + + - - Windows + - :: + + set PYTEST_ADDOPTS=--cov-append + tox + + - - Other + - :: + + PYTEST_ADDOPTS=--cov-append tox diff --git a/baby-color-result.avi b/baby-color-result.avi deleted file mode 100644 index 2444558..0000000 Binary files a/baby-color-result.avi and /dev/null differ diff --git a/baby-motion-result.avi b/baby-motion-result.avi deleted file mode 100644 index 3dc76dd..0000000 Binary files a/baby-motion-result.avi and /dev/null differ diff --git a/baby-result.avi b/baby-result.avi deleted file mode 100644 index 2444558..0000000 Binary files a/baby-result.avi and /dev/null differ diff --git a/baby.mp4 b/baby.mp4 deleted file mode 100644 index 8d4bb6d..0000000 Binary files a/baby.mp4 and /dev/null differ diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd new file mode 100644 index 0000000..289585f --- /dev/null +++ b/ci/appveyor-with-compiler.cmd @@ -0,0 +1,23 @@ +:: Very simple setup: +:: - if WINDOWS_SDK_VERSION is set then activate the SDK. +:: - disable the WDK if it's around. + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" +ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% + +IF EXIST %WIN_WDK% ( + REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ + REN %WIN_WDK% 0wdf +) +IF "%WINDOWS_SDK_VERSION%"=="" GOTO main + +SET DISTUTILS_USE_SDK=1 +SET MSSdk=1 +"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% +CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + +:main +ECHO Executing: %COMMAND_TO_RUN% +CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py new file mode 100755 index 0000000..2597983 --- /dev/null +++ b/ci/bootstrap.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import os +import subprocess +import sys +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import join + +base_path = dirname(dirname(abspath(__file__))) + + +def check_call(args): + print("+", *args) + subprocess.check_call(args) + + +def exec_in_env(): + env_path = join(base_path, ".tox", "bootstrap") + if sys.platform == "win32": + bin_path = join(env_path, "Scripts") + else: + bin_path = join(env_path, "bin") + if not exists(env_path): + import subprocess + + print("Making bootstrap env in: {0} ...".format(env_path)) + try: + check_call([sys.executable, "-m", "venv", env_path]) + except subprocess.CalledProcessError: + try: + check_call([sys.executable, "-m", "virtualenv", env_path]) + except subprocess.CalledProcessError: + check_call(["virtualenv", env_path]) + print("Installing `jinja2` into bootstrap environment...") + check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) + python_executable = join(bin_path, "python") + if not os.path.exists(python_executable): + python_executable += '.exe' + + print("Re-executing with: {0}".format(python_executable)) + print("+ exec", python_executable, __file__, "--no-env") + os.execv(python_executable, [python_executable, __file__, "--no-env"]) + +def main(): + import jinja2 + + print("Project path: {0}".format(base_path)) + + jinja = jinja2.Environment( + loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True + ) + + tox_environments = [ + line.strip() + # 'tox' need not be installed globally, but must be importable + # by the Python that is running this script. + # This uses sys.executable the same way that the call in + # cookiecutter-pylibrary/hooks/post_gen_project.py + # invokes this bootstrap.py itself. + for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() + ] + tox_environments = [line for line in tox_environments if line.startswith('py')] + + for name in os.listdir(join("ci", "templates")): + with open(join(base_path, name), "w") as fh: + fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) + print("Wrote {}".format(name)) + print("DONE.") + + +if __name__ == "__main__": + args = sys.argv[1:] + if args == ["--no-env"]: + main() + elif not args: + exec_in_env() + else: + print("Unexpected arguments {0}".format(args), file=sys.stderr) + sys.exit(1) + diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 0000000..1c8d385 --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1,3 @@ +virtualenv>=16.6.0 +pip>=19.1.1 +setuptools>=18.0.1 diff --git a/ci/templates/.appveyor.yml b/ci/templates/.appveyor.yml new file mode 100644 index 0000000..3206d10 --- /dev/null +++ b/ci/templates/.appveyor.yml @@ -0,0 +1,52 @@ +version: '{branch}-{build}' +build: off +environment: + global: + COVERALLS_EXTRAS: '-v' + COVERALLS_REPO_TOKEN: CmhNs4cpmc7aNcp5FPB4sf379OiXLhufo + matrix: + - TOXENV: check + TOXPYTHON: C:\Python36\python.exe + PYTHON_HOME: C:\Python36 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '32' +{% for env in tox_environments %} +{% if env.startswith(('py2', 'py3')) %} + - TOXENV: {{ env }},codecov,coveralls{{ "" }} + TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe + PYTHON_HOME: C:\Python{{ env[2:4] }} + PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' + PYTHON_ARCH: '32' +{% if 'nocov' in env %} + WHEEL_PATH: .tox/dist +{% endif %} + - TOXENV: {{ env }},codecov,coveralls{{ "" }} + TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe + PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 + PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' + PYTHON_ARCH: '64' +{% if 'nocov' in env %} + WHEEL_PATH: .tox/dist +{% endif %} +{% if env.startswith('py2') %} + WINDOWS_SDK_VERSION: v7.0 +{% endif %} +{% endif %}{% endfor %} +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' + - '%PYTHON_HOME%\Scripts\virtualenv --version' + - '%PYTHON_HOME%\Scripts\easy_install --version' + - '%PYTHON_HOME%\Scripts\pip --version' + - '%PYTHON_HOME%\Scripts\tox --version' +test_script: + - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml new file mode 100644 index 0000000..af7c95c --- /dev/null +++ b/ci/templates/.travis.yml @@ -0,0 +1,46 @@ +language: python +dist: xenial +cache: false +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all +matrix: + include: + - python: '3.6' + env: + - TOXENV=check + - python: '3.6' + env: + - TOXENV=docs +{%- for env in tox_environments %}{{ '' }} + - env: + - TOXENV={{ env }},codecov,coveralls +{%- if env.startswith('pypy3') %}{{ '' }} + - TOXPYTHON=pypy3 + python: 'pypy3' +{%- elif env.startswith('pypy') %}{{ '' }} + python: 'pypy' +{%- else %}{{ '' }} + python: '{{ '{0[2]}.{0[3]}'.format(env) }}' +{%- endif %} +{%- endfor %}{{ '' }} +before_install: + - python --version + - uname -a + - lsb_release -a || true +install: + - python -mpip install --progress-bar=off tox -rci/requirements.txt + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..565b052 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..eceebd8 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.coverage', + 'sphinx.ext.doctest', + 'sphinx.ext.extlinks', + 'sphinx.ext.ifconfig', + 'sphinx.ext.napoleon', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', +] +source_suffix = '.rst' +master_doc = 'index' +project = 'Python Eulerian Video Magnification' +year = '2019-2020' +author = 'Volker G Göhler' +copyright = '{0}, {1}'.format(year, author) +version = release = '0.4.2' + +pygments_style = 'trac' +templates_path = ['.'] +extlinks = { + 'issue': ('https://github.com/vgoehler/PyEVM/issues/%s', '#'), + 'pr': ('https://github.com/vgoehler/PyEVM/pull/%s', 'PR #'), +} +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # only set the theme if we're building docs locally + html_theme = 'sphinx_rtd_theme' + +html_use_smartypants = True +html_last_updated_fmt = '%b %d, %Y' +html_split_index = False +html_sidebars = { + '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], +} +html_short_title = '%s-%s' % (project, version) + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..40f35b5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +======== +Contents +======== + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + reference/index + contributing + authors + changelog + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..fa340ee --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,7 @@ +============ +Installation +============ + +At the command line:: + + pip install PyEVM diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..72a3355 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 0000000..1181b42 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,7 @@ +Reference +========= + +.. toctree:: + :glob: + + python_eulerian_video_magnification* diff --git a/docs/reference/python_eulerian_video_magnification.rst b/docs/reference/python_eulerian_video_magnification.rst new file mode 100644 index 0000000..8d3dfa2 --- /dev/null +++ b/docs/reference/python_eulerian_video_magnification.rst @@ -0,0 +1,9 @@ +python_eulerian_video_magnification +=================================== + +.. testsetup:: + + from python_eulerian_video_magnification import * + +.. automodule:: python_eulerian_video_magnification + :members: diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..37da9ae --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx>=1.3 +sphinx-rtd-theme diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 0000000..f95eb78 --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,11 @@ +builtin +builtins +classmethod +staticmethod +classmethods +staticmethods +args +kwargs +callstack +Changelog +Indices diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..218ec92 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +To use Python Eulerian Video Magnification in a project:: + + import python_eulerian_video_magnification diff --git a/guitar.mp4 b/guitar.mp4 deleted file mode 100644 index 596a622..0000000 Binary files a/guitar.mp4 and /dev/null differ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d6ca63d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,36 @@ +[bdist_wheel] +universal = 1 + +[flake8] +max-line-length = 140 +exclude = */migrations/* + +[tool:pytest] +# If a pytest section is found in one of the possible config files +# (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, +# so if you add a pytest config section elsewhere, +# you will need to delete this section from setup.cfg. +norecursedirs = + migrations + +python_files = + test_*.py + *_test.py + tests.py +addopts = + -ra + --strict + --doctest-modules + --doctest-glob=\*.rst + --tb=short +testpaths = + tests + +[tool:isort] +force_single_line = True +line_length = 120 +known_first_party = python_eulerian_video_magnification +default_section = THIRDPARTY +forced_separate = test_python_eulerian_video_magnification +not_skip = __init__.py +skip = migrations diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e54b529 --- /dev/null +++ b/setup.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function + +import io +import re +from glob import glob +from os.path import basename +from os.path import dirname +from os.path import join +from os.path import splitext + +from setuptools import find_packages +from setuptools import setup + + +def read(*names, **kwargs): + with io.open( + join(dirname(__file__), *names), + encoding=kwargs.get('encoding', 'utf8') + ) as fh: + return fh.read() + + +setup( + name='PyEVM', + version='0.4.2', + license='BSD-2-Clause', + description='Eulerian Video Magnification for Python', + long_description='%s\n%s' % ( + re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) + ), + author='Volker G Göhler', + author_email='volker.goehler@informatik.tu-freiberg.de', + url='https://github.com/vgoehler/PyEVM', + packages=find_packages('src'), + package_dir={'': 'src'}, + py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + include_package_data=True, + zip_safe=False, + classifiers=[ + # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: Unix', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + # 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + # 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: Implementation :: CPython', + # 'Programming Language :: Python :: Implementation :: PyPy', + # uncomment if you test on these interpreters: + # 'Programming Language :: Python :: Implementation :: IronPython', + # 'Programming Language :: Python :: Implementation :: Jython', + # 'Programming Language :: Python :: Implementation :: Stackless', + 'Topic :: Utilities', + 'Private :: Do Not Upload', + ], + project_urls={ + 'Documentation': 'https://PyEVM.readthedocs.io/', + 'Changelog': 'https://PyEVM.readthedocs.io/en/latest/changelog.html', + 'Issue Tracker': 'https://github.com/vgoehler/PyEVM/issues', + }, + keywords=[ + # eg: 'keyword1', 'keyword2', 'keyword3', + ], + python_requires='>=3.6', + install_requires=[ + # eg: 'aspectlib==1.1.1', 'six>=1.7', + 'numpy>=1.18.0', + 'opencv-python>=4.1.2.30', + 'scipy>=1.4.1' + ], + extras_require={ + # eg: + # 'rst': ['docutils>=0.11'], + # ':python_version=="2.6"': ['argparse'], + }, + entry_points={ + 'console_scripts': [ + 'EVM = python_eulerian_video_magnification.cli:main', + ] + }, +) diff --git a/src/python_eulerian_video_magnification/__init__.py b/src/python_eulerian_video_magnification/__init__.py new file mode 100644 index 0000000..a987347 --- /dev/null +++ b/src/python_eulerian_video_magnification/__init__.py @@ -0,0 +1 @@ +__version__ = '0.4.2' diff --git a/src/python_eulerian_video_magnification/__main__.py b/src/python_eulerian_video_magnification/__main__.py new file mode 100644 index 0000000..3720384 --- /dev/null +++ b/src/python_eulerian_video_magnification/__main__.py @@ -0,0 +1,14 @@ +""" +Entrypoint module, in case you use `python -mpython_eulerian_video_magnification`. + + +Why does this file exist, and why __main__? For more info, read: + +- https://www.python.org/dev/peps/pep-0338/ +- https://docs.python.org/2/using/cmdline.html#cmdoption-m +- https://docs.python.org/3/using/cmdline.html#cmdoption-m +""" +from python_eulerian_video_magnification.cli import main + +if __name__ == "__main__": + main() diff --git a/src/python_eulerian_video_magnification/cli.py b/src/python_eulerian_video_magnification/cli.py new file mode 100644 index 0000000..4e8318d --- /dev/null +++ b/src/python_eulerian_video_magnification/cli.py @@ -0,0 +1,154 @@ +""" +Module that contains the command line app. + +Why does this file exist, and why not put this in __main__? + + You might be tempted to import things from __main__ later, but that will cause + problems: the code will get executed twice: + + - When you run `python -mpython_eulerian_video_magnification` python will execute + ``__main__.py`` as a script. That means there won't be any + ``python_eulerian_video_magnification.__main__`` in ``sys.modules``. + - When you import __main__ it will get executed again (as a module) because + there's no ``python_eulerian_video_magnification.__main__`` in ``sys.modules``. + + Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration +""" +import argparse +import logging +import os.path +import sys +from typing import IO + +from python_eulerian_video_magnification.magnifycolor import MagnifyColor +from python_eulerian_video_magnification.magnifymotion import MagnifyMotion +from python_eulerian_video_magnification.metadata import MetaData +from python_eulerian_video_magnification.mode import Mode + + +class CLI: + """The command line interface for evm""" + + def __init__(self): + self.args = None + self.parser = argparse.ArgumentParser( + description='This starts eulerian video magnification on the command line', + epilog='volker.goehler@informatik.tu-freiberg.de', + prog=os.path.split(sys.argv[0])[-1] if '__main__' not in sys.argv[0] else 'eulerian_video_magnification ' + ) + # prog parameter is a fix if the program is called from module level + # io group + io_group = self.parser.add_argument_group("system arguments") + io_group.add_argument('input', type=argparse.FileType('r'), help="the input video file to work on") + # TODO meta information for each file worked on, orig filename, timestamp start and end, size, parameter count + io_group.add_argument('-o', help='output-folder', nargs='?', default=os.path.join(os.path.curdir, "videos"), + type=str) + io_group.add_argument('--color_suffix', help='the suffix to use for color modified result files', nargs='?', + type=str, default='color') + io_group.add_argument('--motion_suffix', help='the suffix to use for motion modified result files', nargs='?', + type=str, default='motion') + io_group.add_argument('--loglevel', help="log level", choices=["debug", "info", "warning", "error", "critical"], + type=str, + default="warning") + + # arguments + arg_group = self.parser.add_argument_group("parameters") + arg_group.add_argument('-m', '--mode', help='the mode of the operation, either enhance colors or motions', + type=Mode.from_string, + choices=list(Mode), default=Mode.COLOR) + arg_group.add_argument('-c', '--low', default=0.4, type=float, help="low parameter (creek)") + arg_group.add_argument('-p', '--high', default=3, type=float, help="high parameter (peek)") + arg_group.add_argument('-l', '--levels', default=3, type=int, help="levels parameter") + arg_group.add_argument('-a', '--amplification', default=20, type=int, help="amplification parameter") + + def parse(self, args: list): + self.args = self.parser.parse_args(args=args) + self.__sanitize_input() + + def __sanitize_input(self): + """ This checks for further conditions in input args """ + self.__check_for_video_file() + self.__manage_output_folder() + + def __check_for_video_file(self): + """ we check if the input file is valid """ + formats = ('avi', 'mpg', 'mpeg', 'mp4') + if os.path.splitext(self.args.input.name)[-1] in (".%s" % ext for ext in formats): + # we got a valid (at least according to extension) file + pass + else: + logging.critical("Input is not a video file. Only supports %s" % ", ".join(formats)) + sys.exit(10) + + def __manage_output_folder(self): + """ in case the output folder is not existent we create it """ + if not os.path.exists(self.output_folder): + os.makedirs(self.output_folder) + + @property + def get_log_level(self) -> int: + """ parses the input loglevel to the numeric value """ + logging.debug(self.args) + return getattr(logging, self.args.loglevel.upper(), None) + + @property + def get_mode(self) -> Mode: + logging.debug(self.args) + return self.args.mode + + @property + def get_file(self) -> IO: + return self.args.input + + @property + def get_low(self) -> float: + return self.args.low + + @property + def get_high(self) -> float: + return self.args.high + + @property + def get_levels(self) -> int: + return self.args.levels + + @property + def get_amplification(self) -> int: + return self.args.amplification + + @property + def output_folder(self) -> str: + return self.args.o + + +def main(args=None): + cli = CLI() + cli.parse(args=args) + + logging.basicConfig(level=cli.get_log_level) + + # create magnification correct Object + if cli.get_mode == Mode.COLOR: + print("Starting Magnification in Color Mode") + magnify = MagnifyColor + suffix = cli.args.color_suffix + elif cli.get_mode == Mode.MOTION: + print("Starting Magnification in Motion Mode") + magnify = MagnifyMotion + suffix = cli.args.motion_suffix + else: + raise NotImplementedError("Unknown Mode") + + meta_data = MetaData( + file_name=cli.get_file.name, + output_folder=cli.args.o, + mode=cli.get_mode, + suffix=suffix, + low=cli.get_low, + high=cli.get_high, + levels=cli.get_levels, + amplification=cli.get_amplification + ) + + work = magnify(meta_data) + work.do_magnify() diff --git a/src/python_eulerian_video_magnification/converter.py b/src/python_eulerian_video_magnification/converter.py new file mode 100644 index 0000000..de742a0 --- /dev/null +++ b/src/python_eulerian_video_magnification/converter.py @@ -0,0 +1,30 @@ +import numpy as np + + +class Converter: + """ + collection of colour space format methods + TODO do we need these? + """ + + @staticmethod + def __convert(src, t_array): + [rows, cols] = src.shape[:2] + dst = np.zeros((rows, cols, 3), dtype=np.float64) + for i in range(rows): + for j in range(cols): + dst[i, j] = np.dot(t_array, src[i, j]) + return dst + + @staticmethod + def rgb2ntsc(src): + t_array = np.array([[0.114, 0.587, 0.298], [-0.321, -0.275, 0.596], [0.311, -0.528, 0.212]]) + return Converter.__convert(src, t_array) + + @staticmethod + def ntsc2rbg(src): + """ + convert YIQ to RGB + """ + t_array = np.array([[1, -1.108, 1.705], [1, -0.272, -0.647], [1, 0.956, 0.620]]) + return Converter.__convert(src, t_array) diff --git a/src/python_eulerian_video_magnification/filter.py b/src/python_eulerian_video_magnification/filter.py new file mode 100644 index 0000000..d4901f7 --- /dev/null +++ b/src/python_eulerian_video_magnification/filter.py @@ -0,0 +1,24 @@ +import numpy as np +from scipy import fftpack as fftpack +from scipy import signal as signal + + +def butter_bandpass_filter(data, lowcut, highcut, fs, order=5): + omega = 0.5 * fs + low = lowcut / omega + high = highcut / omega + b, a = signal.butter(order, [low, high], btype='band') + y = signal.lfilter(b, a, data, axis=0) + return y + + +def temporal_ideal_filter(tensor: np.ndarray, low: float, high: float, fps: int, axis: int = 0) -> np.ndarray: + fft = fftpack.fft(tensor, axis=axis) + frequencies = fftpack.fftfreq(tensor.shape[0], d=1.0 / fps) + bound_low = (np.abs(frequencies - low)).argmin() + bound_high = (np.abs(frequencies - high)).argmin() + fft[:bound_low] = 0 + fft[bound_high:-bound_high] = 0 + fft[-bound_low:] = 0 + iff = fftpack.ifft(fft, axis=axis) + return np.abs(iff) diff --git a/src/python_eulerian_video_magnification/magnify.py b/src/python_eulerian_video_magnification/magnify.py new file mode 100644 index 0000000..3de2232 --- /dev/null +++ b/src/python_eulerian_video_magnification/magnify.py @@ -0,0 +1,67 @@ +import cv2 +import numpy as np + +from python_eulerian_video_magnification.metadata import MetaData + + +class Magnify: + def __init__(self, data: MetaData): + self._data = data + + def load_video(self) -> (np.ndarray, int): + cap = cv2.VideoCapture(self._in_file_name) + frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + width, height = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = int(cap.get(cv2.CAP_PROP_FPS)) + video_tensor = np.zeros((frame_count, height, width, 3), dtype='float') + x = 0 + while cap.isOpened(): + ret, frame = cap.read() + if ret is True: + video_tensor[x] = frame + x += 1 + else: + break + return video_tensor, fps + + def save_video(self, video_tensor: np.ndarray) -> None: + # four_cc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') + four_cc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') + [height, width] = video_tensor[0].shape[0:2] + writer = cv2.VideoWriter(self._out_file_name, four_cc, 30, (width, height), 1) + for i in range(0, video_tensor.shape[0]): + writer.write(cv2.convertScaleAbs(video_tensor[i])) + writer.release() + + def do_magnify(self) -> None: + tensor, fps = self.load_video() + video_tensor = self._magnify_impl(tensor, fps) + self.save_video(video_tensor) + self._data.save_meta_data() + + def _magnify_impl(self, tensor: np.ndarray, fps: int) -> np.ndarray: + raise NotImplementedError("This should be overwritten!") + + @property + def _low(self) -> float: + return self._data['low'] + + @property + def _high(self) -> float: + return self._data['high'] + + @property + def _levels(self) -> int: + return self._data['levels'] + + @property + def _amplification(self) -> float: + return self._data['amplification'] + + @property + def _in_file_name(self) -> str: + return self._data['file'] + + @property + def _out_file_name(self) -> str: + return self._data['target'] diff --git a/src/python_eulerian_video_magnification/magnifycolor.py b/src/python_eulerian_video_magnification/magnifycolor.py new file mode 100644 index 0000000..bd194ce --- /dev/null +++ b/src/python_eulerian_video_magnification/magnifycolor.py @@ -0,0 +1,45 @@ +import cv2 +import numpy as np + +from python_eulerian_video_magnification.filter import temporal_ideal_filter +from python_eulerian_video_magnification.magnify import Magnify +from python_eulerian_video_magnification.pyramid import gaussian_video + + +class MagnifyColor(Magnify): + def _magnify_impl(self, tensor: np.ndarray, fps: int) -> np.ndarray: + gau_video = gaussian_video(tensor, levels=self._levels) + filtered_tensor = temporal_ideal_filter(gau_video, self._low, self._high, fps) + amplified_video = self._amplify_video(filtered_tensor) + return self._reconstruct_video(amplified_video, tensor) + + def _amplify_video(self, gaussian_vid): + return gaussian_vid * self._amplification + + def _reconstruct_video(self, amp_video, origin_video): + origin_video_shape = origin_video.shape[1:] + for i in range(0, amp_video.shape[0]): + img = amp_video[i] + for x in range(self._levels): + img = cv2.pyrUp(img) # this doubles the dimensions of img each time + # ensure that dimensions are equal + origin_video[i] += self._correct_dimensionality_problem_after_pyr_up(img, origin_video_shape) + return origin_video + + def _correct_dimensionality_problem_after_pyr_up(self, img: np.ndarray, origin_video_frame_shape) -> np.ndarray: + if img.shape != origin_video_frame_shape: + return np.resize(img, origin_video_frame_shape) + else: + return img + + def principal_component_analysis(self, tensor: np.ndarray): + # Data matrix tensor, assumes 0-centered + n, m = tensor.shape + assert np.allclose(tensor.mean(axis=0), np.zeros(m)) + # Compute covariance matrix + covariance_matrix = np.dot(tensor.T, tensor) / (n - 1) + # Eigen decomposition + eigen_vals, eigen_vecs = np.linalg.eig(covariance_matrix) + # Project tensor onto PC space + X_pca = np.dot(tensor, eigen_vecs) + return X_pca diff --git a/src/python_eulerian_video_magnification/magnifymotion.py b/src/python_eulerian_video_magnification/magnifymotion.py new file mode 100644 index 0000000..19ab918 --- /dev/null +++ b/src/python_eulerian_video_magnification/magnifymotion.py @@ -0,0 +1,27 @@ +import cv2 +import numpy as np + +from python_eulerian_video_magnification.filter import butter_bandpass_filter +from python_eulerian_video_magnification.magnify import Magnify +from python_eulerian_video_magnification.pyramid import laplacian_video + + +class MagnifyMotion(Magnify): + def _magnify_impl(self, tensor: np.ndarray, fps: int) -> np.ndarray: + lap_video_list = laplacian_video(tensor, levels=self._levels) + filter_tensor_list = [] + for i in range(self._levels): + filter_tensor = butter_bandpass_filter(lap_video_list[i], self._low, self._high, fps) + filter_tensor *= self._amplification + filter_tensor_list.append(filter_tensor) + recon = self._reconstruct_from_tensor_list(filter_tensor_list) + return tensor + recon + + def _reconstruct_from_tensor_list(self, filter_tensor_list): + final = np.zeros(filter_tensor_list[-1].shape) + for i in range(filter_tensor_list[0].shape[0]): + up = filter_tensor_list[0][i] + for n in range(self._levels - 1): + up = cv2.pyrUp(up) + filter_tensor_list[n + 1][i] + final[i] = up + return final diff --git a/src/python_eulerian_video_magnification/metadata.py b/src/python_eulerian_video_magnification/metadata.py new file mode 100644 index 0000000..2874f23 --- /dev/null +++ b/src/python_eulerian_video_magnification/metadata.py @@ -0,0 +1,51 @@ +import json +import os.path +from datetime import datetime + +from python_eulerian_video_magnification.mode import Mode + + +class MetaData: + + def __init__(self, file_name: str, output_folder: str, + mode: Mode, suffix: str, + low: float, high: float, + levels: int = 3, amplification: int = 20 + ): + self.__data = { + 'file': file_name, + 'output': output_folder, + 'target': MetaData.output_file_name(file_name, suffix=suffix, path=output_folder), + 'meta_target': MetaData.output_file_name(file_name, suffix=suffix, path=output_folder, generate_json=True), + 'low': low, + 'high': high, + 'levels': levels, + 'amplification': amplification, + 'mode': mode.name, + 'suffix': suffix, + 'date': None, + } + + @staticmethod + def output_file_name(filename: str, suffix: str, path: str, generate_json=False): + filename_split = os.path.splitext(os.path.split(filename)[-1]) + extension = ".json" if generate_json else filename_split[1] + return os.path.join(path, filename_split[0] + "_%s_evm_%s" % + (suffix, MetaData.format_date(MetaData.get_date())) + extension) + + def __getitem__(self, item): + return self.__data[item] + + def save_meta_data(self): + """stores the meta data dictionary as a json""" + self.__data['date'] = MetaData.format_date(MetaData.get_date()) + with open(self.__data['meta_target'], 'w') as fp: + json.dump(self.__data, fp=fp, sort_keys=True, indent=4, separators=(',', ': ')) + + @staticmethod + def get_date() -> datetime: + return datetime.now() + + @staticmethod + def format_date(date: datetime) -> str: + return date.strftime("%Y-%m-%d-%H-%M-%S") diff --git a/src/python_eulerian_video_magnification/mode.py b/src/python_eulerian_video_magnification/mode.py new file mode 100644 index 0000000..334b4da --- /dev/null +++ b/src/python_eulerian_video_magnification/mode.py @@ -0,0 +1,17 @@ +import enum + + +@enum.unique +class Mode(enum.Enum): + COLOR = 1 + MOTION = 2 + + def __str__(self): + return self.name + + @staticmethod + def from_string(s): + try: + return Mode[s.upper()] + except KeyError: + raise ValueError() diff --git a/src/python_eulerian_video_magnification/pyramid.py b/src/python_eulerian_video_magnification/pyramid.py new file mode 100644 index 0000000..512c533 --- /dev/null +++ b/src/python_eulerian_video_magnification/pyramid.py @@ -0,0 +1,45 @@ +import cv2 +import numpy as np + + +def build_gaussian_pyramid(src, level=3): + s = src.copy() + pyramid = [s] + for i in range(level): + s = cv2.pyrDown(s) + pyramid.append(s) + return pyramid + + +def build_laplacian_pyramid(src, levels=3): + gaussianPyramid = build_gaussian_pyramid(src, levels) + pyramid = [] + for i in range(levels, 0, -1): + GE = cv2.pyrUp(gaussianPyramid[i]) + L = cv2.subtract(gaussianPyramid[i - 1], GE) + pyramid.append(L) + return pyramid + + +def gaussian_video(video_tensor, levels=3): + for i in range(0, video_tensor.shape[0]): + frame = video_tensor[i] + pyr = build_gaussian_pyramid(frame, level=levels) + gaussian_frame = pyr[-1] + if i == 0: + vid_data = np.zeros((video_tensor.shape[0], gaussian_frame.shape[0], gaussian_frame.shape[1], 3)) + vid_data[i] = gaussian_frame + return vid_data + + +def laplacian_video(video_tensor, levels=3): + tensor_list = [] + for i in range(0, video_tensor.shape[0]): + frame = video_tensor[i] + pyr = build_laplacian_pyramid(frame, levels=levels) + if i == 0: + for k in range(levels): + tensor_list.append(np.zeros((video_tensor.shape[0], pyr[k].shape[0], pyr[k].shape[1], 3))) + for n in range(levels): + tensor_list[n][i] = pyr[n] + return tensor_list diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..808e8c3 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,73 @@ +import functools +import logging +import os.path + +import pytest + +from python_eulerian_video_magnification import cli +from python_eulerian_video_magnification.mode import Mode + +base_path = functools.partial(os.path.join, os.getcwd(), 'tests', 'videos') +sample = base_path('guitar1sec.mp4') +wrong_video = base_path('wrong_video.txt') + + +def test_for_correctly_set_file(): + sut = cli.CLI() + sut.parse(args=[sample]) + assert sut.get_file.name == sample + + +def test_for_wrong_file(): + sut = cli.CLI() + with pytest.raises(SystemExit) as e: + sut.parse(args=[wrong_video]) + assert "10" in str(e.value) + + +def test_check_mode(): + sut = cli.CLI() + sut.parse(args=[sample, '-m', 'motion']) + assert sut.get_mode == Mode.MOTION + + +def test_mode_default(): + sut = cli.CLI() + sut.parse(args=[sample]) + assert sut.get_mode == Mode.COLOR + + +def test_log_level_set(): + sut = cli.CLI() + sut.parse(args=[sample, '--loglevel=warning']) + assert sut.get_log_level == logging.WARNING, "log level should be warning" + + +def test_log_level_wrong(): + sut = cli.CLI() + with pytest.raises(SystemExit) as e: + sut.parse(args=[sample, '--loglevel=fubar']) + assert "2" in str(e.value) + + +@pytest.mark.parametrize( + "value, parameter_flag, method", + [ + (0.5, "-c", 'get_low'), + (3.2, '-p', 'get_high'), + (5, '-l', 'get_levels'), + (10, '-a', 'get_amplification') + ] +) +def test_correctly_set_numeric(value, parameter_flag, method): + sut = cli.CLI() + sut.parse(args=[sample, '%s %s' % (parameter_flag, value)]) + assert getattr(sut, method) == value + + +def test_for_invalid_output_path(tmp_path): + sut = cli.CLI() + path = os.path.join(tmp_path, 'not_existent.path') + assert not os.path.exists(path) + sut.parse(args=[sample, '-o', '%s' % path]) + assert os.path.exists(path) diff --git a/tests/test_magnify.py b/tests/test_magnify.py new file mode 100644 index 0000000..0e4b866 --- /dev/null +++ b/tests/test_magnify.py @@ -0,0 +1,26 @@ +import pytest + +from python_eulerian_video_magnification.magnify import Magnify +from python_eulerian_video_magnification.metadata import MetaData +from python_eulerian_video_magnification.mode import Mode + + +@pytest.fixture +def magnify_sut(): + data = MetaData(file_name="fubar", output_folder="/fu/bar", mode=Mode.COLOR, suffix="color", + low=0.1, high=3.1, levels=2, + amplification=23) + return Magnify(data=data) + + +@pytest.mark.parametrize( + "property_to_test, value", + [ + ("_low", 0.1), + ("_high", 3.1), + ("_levels", 2), + ("_amplification", 23) + ] +) +def test_properties(property_to_test, value, magnify_sut): + assert getattr(magnify_sut, property_to_test) == value diff --git a/tests/test_magnify_color.py b/tests/test_magnify_color.py new file mode 100644 index 0000000..e843413 --- /dev/null +++ b/tests/test_magnify_color.py @@ -0,0 +1,47 @@ +import numpy as np +from numpy import testing + +from python_eulerian_video_magnification.magnifycolor import MagnifyColor +from python_eulerian_video_magnification.metadata import MetaData +from python_eulerian_video_magnification.mode import Mode + + +def test_reconstruct_video(): + original = np.ones((32, 200, 300, 3), dtype='float') + modified = np.ones((32, 100, 150, 3), dtype='float') + # the pyramid up with only 1 level will double the dimensions of modified exactly one time + + meta = MetaData( + file_name="/fu/bar/gob.avi", + output_folder="/out/put/", + mode=Mode.COLOR, + suffix="color", + low=0.1, + high=2.1, + levels=1, + amplification=23 + ) + magnify = MagnifyColor(data=meta) + + final = magnify._reconstruct_video(modified, original) + testing.assert_array_equal(final, np.full((32, 200, 300, 3), 2)) + + +def test_for_pyrup_rounding_error_fix(): + image = np.ones((100, 150, 3), dtype='float') + + meta = MetaData( + file_name="/fu/bar/gob.avi", + output_folder="/out/put/", + mode=Mode.COLOR, + suffix="color", + low=0.1, + high=2.1, + levels=1, + amplification=23 + ) + magnify = MagnifyColor(data=meta) + + dimensions = (200, 300, 3) + solution = magnify._correct_dimensionality_problem_after_pyr_up(image, dimensions) + testing.assert_equal(dimensions, solution.shape) diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..2722e89 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,62 @@ +import os.path +from datetime import datetime + +from python_eulerian_video_magnification.metadata import MetaData +from python_eulerian_video_magnification.mode import Mode + + +def test_output_filename(monkeypatch): + monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, + microsecond=1)) + out = MetaData.output_file_name("abd.avi", "color", "/fu/bar/") + assert out == "/fu/bar/abd_color_evm_2020-01-01-16-16-16.avi" + + +def test_output_filename_with_path(monkeypatch): + monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, + microsecond=1)) + out = MetaData.output_file_name("/tmp/abd.avi", "color", "/fu/bar/") + assert out == "/fu/bar/abd_color_evm_2020-01-01-16-16-16.avi" + + +def test_output_filename_with_path_for_metadata(monkeypatch): + monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, + microsecond=1)) + out = MetaData.output_file_name("/tmp/abd.avi", "color", "/fu/bar/", generate_json=True) + assert out == "/fu/bar/abd_color_evm_2020-01-01-16-16-16.json" + + +def test_for_data_as_expected(monkeypatch): + monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, + microsecond=1)) + sut = MetaData( + file_name="/fu/bar/gob.avi", + output_folder="/out/put/", + mode=Mode.COLOR, + suffix="color", + low=0.1, + high=2.1, + levels=4, + amplification=23 + ) + assert getattr(sut, "_MetaData__data") == {'file': "/fu/bar/gob.avi", 'output': "/out/put/", + 'target': "/out/put/gob_color_evm_2020-01-01-16-16-16.avi", + 'meta_target': "/out/put/gob_color_evm_2020-01-01-16-16-16.json", + 'low': 0.1, 'high': 2.1, + 'levels': 4, + 'amplification': 23, 'mode': 'COLOR', 'suffix': 'color', 'date': None} + + +def test_json_save(tmp_path): + sut = MetaData( + file_name="/fu/bar/gob.avi", + output_folder=str(tmp_path), + mode=Mode.COLOR, + suffix="color", + low=0.1, + high=2.1, + levels=4, + amplification=23 + ) + sut.save_meta_data() + assert os.path.exists(sut['meta_target']) diff --git a/tests/test_python_eulerian_video_magnification.py b/tests/test_python_eulerian_video_magnification.py new file mode 100644 index 0000000..4699308 --- /dev/null +++ b/tests/test_python_eulerian_video_magnification.py @@ -0,0 +1,15 @@ + +import pytest + +from python_eulerian_video_magnification.cli import main + + +def test_main_as_smoke_test_wrong_filename(capsys): + """This tests the setup of main for wrong input file only as a smoke test, as I'm not testing arparse""" + with pytest.raises(SystemExit) as e: + main(['fu.bar']) + + captured = capsys.readouterr() + + assert "No such file or directory: 'fu.bar'" in captured.err + assert 2 == e.value.code diff --git a/tests/test_video_motion_handling.py b/tests/test_video_motion_handling.py new file mode 100644 index 0000000..0e9690f --- /dev/null +++ b/tests/test_video_motion_handling.py @@ -0,0 +1,17 @@ +import os +from datetime import datetime + +from python_eulerian_video_magnification.cli import main +from python_eulerian_video_magnification.metadata import MetaData + +sample = os.path.join(os.getcwd(), 'tests', 'videos', 'guitar1sec.mp4') + + +def test_process_video(tmp_path, capsys, monkeypatch): + monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, microsecond=1)) + main([sample, '-o', str(tmp_path)]) + + captured = capsys.readouterr() + + assert "Starting Magnification in Color Mode" in captured.out + assert os.path.isfile(MetaData.output_file_name(filename=sample, suffix="color", path=tmp_path)) diff --git a/tests/videos/guitar1sec.mp4 b/tests/videos/guitar1sec.mp4 new file mode 100644 index 0000000..270292a Binary files /dev/null and b/tests/videos/guitar1sec.mp4 differ diff --git a/tests/videos/wrong_video.txt b/tests/videos/wrong_video.txt new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..47a3fd1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,90 @@ +[testenv:bootstrap] +deps = + jinja2 + matrix + tox +skip_install = true +commands = + python ci/bootstrap.py --no-env +passenv = + * +; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist + +[tox] +envlist = + clean, + check, + docs, + {py36,py37,py38}, + report +ignore_basepython_conflict = true + +[testenv] +basepython = + py36: {env:TOXPYTHON:python3.6} + {py37,docs}: {env:TOXPYTHON:python3.7} + py38: {env:TOXPYTHON:python3.8} + {bootstrap,clean,check,report,codecov,coveralls}: {env:TOXPYTHON:python3} +setenv = + PYTHONPATH={toxinidir}/tests + PYTHONUNBUFFERED=yes +passenv = + * +usedevelop = false +deps = + pytest + pytest-travis-fold + pytest-cov +commands = + {posargs:pytest --cov --cov-report=term-missing -vv tests} + +[testenv:check] +deps = + twine + wheel + check-manifest + flake8 + readme-renderer + pygments + isort +skip_install = true +commands = + python setup.py bdist_wheel sdist + twine check dist/*.whl dist/*.tar.gz + check-manifest {toxinidir} + flake8 src tests setup.py + isort --verbose --check-only --diff --recursive src tests setup.py + +[testenv:docs] +usedevelop = true +deps = + -r{toxinidir}/docs/requirements.txt +commands = + sphinx-build {posargs:-E} -b html docs dist/docs + sphinx-build -b linkcheck docs dist/docs + +[testenv:coveralls] +deps = + coveralls +skip_install = true +commands = + coveralls [] + +[testenv:codecov] +deps = + codecov +skip_install = true +commands = + codecov [] + +[testenv:report] +deps = coverage +skip_install = true +commands = + coverage report + coverage html + +[testenv:clean] +commands = coverage erase +skip_install = true +deps = coverage