├── tests ├── videos │ ├── wrong_video.txt │ └── guitar1sec.mp4 ├── test_python_eulerian_video_magnification.py ├── test_video_motion_handling.py ├── test_magnify.py ├── test_magnify_color.py ├── test_cli.py └── test_metadata.py ├── docs ├── authors.rst ├── readme.rst ├── changelog.rst ├── contributing.rst ├── requirements.txt ├── installation.rst ├── reference │ ├── index.rst │ └── python_eulerian_video_magnification.rst ├── usage.rst ├── spelling_wordlist.txt ├── index.rst └── conf.py ├── src └── python_eulerian_video_magnification │ ├── __init__.py │ ├── mode.py │ ├── __main__.py │ ├── filter.py │ ├── converter.py │ ├── magnifymotion.py │ ├── pyramid.py │ ├── metadata.py │ ├── magnifycolor.py │ ├── magnify.py │ └── cli.py ├── ci ├── requirements.txt ├── appveyor-with-compiler.cmd ├── templates │ ├── .travis.yml │ └── .appveyor.yml └── bootstrap.py ├── CHANGELOG.rst ├── AUTHORS.rst ├── .coveragerc ├── .editorconfig ├── MANIFEST.in ├── .bumpversion.cfg ├── setup.cfg ├── .gitignore ├── .github └── workflows │ └── pythonapp.yml ├── .travis.yml ├── LICENSE ├── tox.ini ├── .appveyor.yml ├── CONTRIBUTING.rst ├── .cookiecutterrc ├── setup.py └── README.rst /tests/videos/wrong_video.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | sphinx-rtd-theme 3 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.4.2' 2 | -------------------------------------------------------------------------------- /ci/requirements.txt: -------------------------------------------------------------------------------- 1 | virtualenv>=16.6.0 2 | pip>=19.1.1 3 | setuptools>=18.0.1 4 | -------------------------------------------------------------------------------- /tests/videos/guitar1sec.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vgoehler/PyEVM/HEAD/tests/videos/guitar1sec.mp4 -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 5 | 0.1.0 (2020-01-02) 6 | ------------------ 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install PyEVM 8 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | python_eulerian_video_magnification* 8 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use Python Eulerian Video Magnification in a project:: 6 | 7 | import python_eulerian_video_magnification 8 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Volker G Göhler - https://github.com/vgoehler 6 | 7 | EVM Project Forked from 8 | ----------------------- 9 | 10 | * flyingzhao - https://github.com/flyingzhao/PyEVM 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src 4 | */site-packages 5 | 6 | [run] 7 | branch = true 8 | source = 9 | python_eulerian_video_magnification 10 | tests 11 | parallel = true 12 | 13 | [report] 14 | show_missing = true 15 | precision = 2 16 | omit = *migrations* 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /docs/reference/python_eulerian_video_magnification.rst: -------------------------------------------------------------------------------- 1 | python_eulerian_video_magnification 2 | =================================== 3 | 4 | .. testsetup:: 5 | 6 | from python_eulerian_video_magnification import * 7 | 8 | .. automodule:: python_eulerian_video_magnification 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | readme 9 | installation 10 | usage 11 | reference/index 12 | contributing 13 | authors 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/mode.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | @enum.unique 5 | class Mode(enum.Enum): 6 | COLOR = 1 7 | MOTION = 2 8 | 9 | def __str__(self): 10 | return self.name 11 | 12 | @staticmethod 13 | def from_string(s): 14 | try: 15 | return Mode[s.upper()] 16 | except KeyError: 17 | raise ValueError() 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft src 3 | graft ci 4 | graft tests 5 | 6 | include .bumpversion.cfg 7 | include .coveragerc 8 | include .cookiecutterrc 9 | include .editorconfig 10 | 11 | include AUTHORS.rst 12 | include CHANGELOG.rst 13 | include CONTRIBUTING.rst 14 | include LICENSE 15 | include README.rst 16 | 17 | include tox.ini .travis.yml .appveyor.yml 18 | 19 | global-exclude *.py[cod] __pycache__/* *.so *.dylib 20 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Entrypoint module, in case you use `python -mpython_eulerian_video_magnification`. 3 | 4 | 5 | Why does this file exist, and why __main__? For more info, read: 6 | 7 | - https://www.python.org/dev/peps/pep-0338/ 8 | - https://docs.python.org/2/using/cmdline.html#cmdoption-m 9 | - https://docs.python.org/3/using/cmdline.html#cmdoption-m 10 | """ 11 | from python_eulerian_video_magnification.cli import main 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /tests/test_python_eulerian_video_magnification.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | 4 | from python_eulerian_video_magnification.cli import main 5 | 6 | 7 | def test_main_as_smoke_test_wrong_filename(capsys): 8 | """This tests the setup of main for wrong input file only as a smoke test, as I'm not testing arparse""" 9 | with pytest.raises(SystemExit) as e: 10 | main(['fu.bar']) 11 | 12 | captured = capsys.readouterr() 13 | 14 | assert "No such file or directory: 'fu.bar'" in captured.err 15 | assert 2 == e.value.code 16 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.4.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:README.rst] 11 | search = v{current_version}. 12 | replace = v{new_version}. 13 | 14 | [bumpversion:file:docs/conf.py] 15 | search = version = release = '{current_version}' 16 | replace = version = release = '{new_version}' 17 | 18 | [bumpversion:file:src/python_eulerian_video_magnification/__init__.py] 19 | search = __version__ = '{current_version}' 20 | replace = __version__ = '{new_version}' 21 | 22 | -------------------------------------------------------------------------------- /tests/test_video_motion_handling.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | from python_eulerian_video_magnification.cli import main 5 | from python_eulerian_video_magnification.metadata import MetaData 6 | 7 | sample = os.path.join(os.getcwd(), 'tests', 'videos', 'guitar1sec.mp4') 8 | 9 | 10 | def test_process_video(tmp_path, capsys, monkeypatch): 11 | monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, microsecond=1)) 12 | main([sample, '-o', str(tmp_path)]) 13 | 14 | captured = capsys.readouterr() 15 | 16 | assert "Starting Magnification in Color Mode" in captured.out 17 | assert os.path.isfile(MetaData.output_file_name(filename=sample, suffix="color", path=tmp_path)) 18 | -------------------------------------------------------------------------------- /tests/test_magnify.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from python_eulerian_video_magnification.magnify import Magnify 4 | from python_eulerian_video_magnification.metadata import MetaData 5 | from python_eulerian_video_magnification.mode import Mode 6 | 7 | 8 | @pytest.fixture 9 | def magnify_sut(): 10 | data = MetaData(file_name="fubar", output_folder="/fu/bar", mode=Mode.COLOR, suffix="color", 11 | low=0.1, high=3.1, levels=2, 12 | amplification=23) 13 | return Magnify(data=data) 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "property_to_test, value", 18 | [ 19 | ("_low", 0.1), 20 | ("_high", 3.1), 21 | ("_levels", 2), 22 | ("_amplification", 23) 23 | ] 24 | ) 25 | def test_properties(property_to_test, value, magnify_sut): 26 | assert getattr(magnify_sut, property_to_test) == value 27 | -------------------------------------------------------------------------------- /ci/appveyor-with-compiler.cmd: -------------------------------------------------------------------------------- 1 | :: Very simple setup: 2 | :: - if WINDOWS_SDK_VERSION is set then activate the SDK. 3 | :: - disable the WDK if it's around. 4 | 5 | SET COMMAND_TO_RUN=%* 6 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 7 | SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" 8 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 9 | 10 | IF EXIST %WIN_WDK% ( 11 | REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ 12 | REN %WIN_WDK% 0wdf 13 | ) 14 | IF "%WINDOWS_SDK_VERSION%"=="" GOTO main 15 | 16 | SET DISTUTILS_USE_SDK=1 17 | SET MSSdk=1 18 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 19 | CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 20 | 21 | :main 22 | ECHO Executing: %COMMAND_TO_RUN% 23 | CALL %COMMAND_TO_RUN% || EXIT 1 24 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/filter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import fftpack as fftpack 3 | from scipy import signal as signal 4 | 5 | 6 | def butter_bandpass_filter(data, lowcut, highcut, fs, order=5): 7 | omega = 0.5 * fs 8 | low = lowcut / omega 9 | high = highcut / omega 10 | b, a = signal.butter(order, [low, high], btype='band') 11 | y = signal.lfilter(b, a, data, axis=0) 12 | return y 13 | 14 | 15 | def temporal_ideal_filter(tensor: np.ndarray, low: float, high: float, fps: int, axis: int = 0) -> np.ndarray: 16 | fft = fftpack.fft(tensor, axis=axis) 17 | frequencies = fftpack.fftfreq(tensor.shape[0], d=1.0 / fps) 18 | bound_low = (np.abs(frequencies - low)).argmin() 19 | bound_high = (np.abs(frequencies - high)).argmin() 20 | fft[:bound_low] = 0 21 | fft[bound_high:-bound_high] = 0 22 | fft[-bound_low:] = 0 23 | iff = fftpack.ifft(fft, axis=axis) 24 | return np.abs(iff) 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 140 6 | exclude = */migrations/* 7 | 8 | [tool:pytest] 9 | # If a pytest section is found in one of the possible config files 10 | # (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, 11 | # so if you add a pytest config section elsewhere, 12 | # you will need to delete this section from setup.cfg. 13 | norecursedirs = 14 | migrations 15 | 16 | python_files = 17 | test_*.py 18 | *_test.py 19 | tests.py 20 | addopts = 21 | -ra 22 | --strict 23 | --doctest-modules 24 | --doctest-glob=\*.rst 25 | --tb=short 26 | testpaths = 27 | tests 28 | 29 | [tool:isort] 30 | force_single_line = True 31 | line_length = 120 32 | known_first_party = python_eulerian_video_magnification 33 | default_section = THIRDPARTY 34 | forced_separate = test_python_eulerian_video_magnification 35 | not_skip = __init__.py 36 | skip = migrations 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | wheelhouse 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | venv*/ 24 | pyvenv*/ 25 | pip-wheel-metadata/ 26 | 27 | # Installer logs 28 | pip-log.txt 29 | 30 | # Unit test / coverage reports 31 | .coverage 32 | .tox 33 | .coverage.* 34 | .pytest_cache/ 35 | nosetests.xml 36 | coverage.xml 37 | htmlcov 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | .idea 47 | *.iml 48 | *.komodoproject 49 | 50 | # Complexity 51 | output/*.html 52 | output/*/index.html 53 | 54 | # Sphinx 55 | docs/_build 56 | 57 | .DS_Store 58 | *~ 59 | .*.sw[po] 60 | .build 61 | .ve 62 | .env 63 | .cache 64 | .pytest 65 | .benchmarks 66 | .bootstrap 67 | .appveyor.token 68 | *.bak 69 | 70 | # Mypy Cache 71 | .mypy_cache/ 72 | videos/ 73 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | # stop the build if there are Python syntax errors or undefined names 24 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 25 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 26 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 27 | - name: Test with pytest 28 | run: | 29 | pip install pytest 30 | pytest 31 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/converter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Converter: 5 | """ 6 | collection of colour space format methods 7 | TODO do we need these? 8 | """ 9 | 10 | @staticmethod 11 | def __convert(src, t_array): 12 | [rows, cols] = src.shape[:2] 13 | dst = np.zeros((rows, cols, 3), dtype=np.float64) 14 | for i in range(rows): 15 | for j in range(cols): 16 | dst[i, j] = np.dot(t_array, src[i, j]) 17 | return dst 18 | 19 | @staticmethod 20 | def rgb2ntsc(src): 21 | t_array = np.array([[0.114, 0.587, 0.298], [-0.321, -0.275, 0.596], [0.311, -0.528, 0.212]]) 22 | return Converter.__convert(src, t_array) 23 | 24 | @staticmethod 25 | def ntsc2rbg(src): 26 | """ 27 | convert YIQ to RGB 28 | """ 29 | t_array = np.array([[1, -1.108, 1.705], [1, -0.272, -0.647], [1, 0.956, 0.620]]) 30 | return Converter.__convert(src, t_array) 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | cache: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | include: 10 | - python: '3.6' 11 | env: 12 | - TOXENV=check 13 | - python: '3.7' 14 | env: 15 | - TOXENV=docs 16 | - env: 17 | - TOXENV=py36,codecov,coveralls 18 | python: '3.6' 19 | - env: 20 | - TOXENV=py37,codecov,coveralls 21 | python: '3.7' 22 | - env: 23 | - TOXENV=py38,codecov,coveralls 24 | python: '3.8' 25 | before_install: 26 | - python --version 27 | - uname -a 28 | - lsb_release -a || true 29 | install: 30 | - python -mpip install --progress-bar=off tox -rci/requirements.txt 31 | - virtualenv --version 32 | - easy_install --version 33 | - pip --version 34 | - tox --version 35 | script: 36 | - tox -v 37 | after_failure: 38 | - more .tox/log/* | cat 39 | - more .tox/*/log/* | cat 40 | notifications: 41 | email: 42 | on_success: never 43 | on_failure: always 44 | -------------------------------------------------------------------------------- /ci/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | cache: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | include: 10 | - python: '3.6' 11 | env: 12 | - TOXENV=check 13 | - python: '3.6' 14 | env: 15 | - TOXENV=docs 16 | {%- for env in tox_environments %}{{ '' }} 17 | - env: 18 | - TOXENV={{ env }},codecov,coveralls 19 | {%- if env.startswith('pypy3') %}{{ '' }} 20 | - TOXPYTHON=pypy3 21 | python: 'pypy3' 22 | {%- elif env.startswith('pypy') %}{{ '' }} 23 | python: 'pypy' 24 | {%- else %}{{ '' }} 25 | python: '{{ '{0[2]}.{0[3]}'.format(env) }}' 26 | {%- endif %} 27 | {%- endfor %}{{ '' }} 28 | before_install: 29 | - python --version 30 | - uname -a 31 | - lsb_release -a || true 32 | install: 33 | - python -mpip install --progress-bar=off tox -rci/requirements.txt 34 | - virtualenv --version 35 | - easy_install --version 36 | - pip --version 37 | - tox --version 38 | script: 39 | - tox -v 40 | after_failure: 41 | - more .tox/log/* | cat 42 | - more .tox/*/log/* | cat 43 | notifications: 44 | email: 45 | on_success: never 46 | on_failure: always 47 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/magnifymotion.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | from python_eulerian_video_magnification.filter import butter_bandpass_filter 5 | from python_eulerian_video_magnification.magnify import Magnify 6 | from python_eulerian_video_magnification.pyramid import laplacian_video 7 | 8 | 9 | class MagnifyMotion(Magnify): 10 | def _magnify_impl(self, tensor: np.ndarray, fps: int) -> np.ndarray: 11 | lap_video_list = laplacian_video(tensor, levels=self._levels) 12 | filter_tensor_list = [] 13 | for i in range(self._levels): 14 | filter_tensor = butter_bandpass_filter(lap_video_list[i], self._low, self._high, fps) 15 | filter_tensor *= self._amplification 16 | filter_tensor_list.append(filter_tensor) 17 | recon = self._reconstruct_from_tensor_list(filter_tensor_list) 18 | return tensor + recon 19 | 20 | def _reconstruct_from_tensor_list(self, filter_tensor_list): 21 | final = np.zeros(filter_tensor_list[-1].shape) 22 | for i in range(filter_tensor_list[0].shape[0]): 23 | up = filter_tensor_list[0][i] 24 | for n in range(self._levels - 1): 25 | up = cv2.pyrUp(up) + filter_tensor_list[n + 1][i] 26 | final[i] = up 27 | return final 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019-2020, Volker G Göhler. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 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. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | extensions = [ 7 | 'sphinx.ext.autodoc', 8 | 'sphinx.ext.autosummary', 9 | 'sphinx.ext.coverage', 10 | 'sphinx.ext.doctest', 11 | 'sphinx.ext.extlinks', 12 | 'sphinx.ext.ifconfig', 13 | 'sphinx.ext.napoleon', 14 | 'sphinx.ext.todo', 15 | 'sphinx.ext.viewcode', 16 | ] 17 | source_suffix = '.rst' 18 | master_doc = 'index' 19 | project = 'Python Eulerian Video Magnification' 20 | year = '2019-2020' 21 | author = 'Volker G Göhler' 22 | copyright = '{0}, {1}'.format(year, author) 23 | version = release = '0.4.2' 24 | 25 | pygments_style = 'trac' 26 | templates_path = ['.'] 27 | extlinks = { 28 | 'issue': ('https://github.com/vgoehler/PyEVM/issues/%s', '#'), 29 | 'pr': ('https://github.com/vgoehler/PyEVM/pull/%s', 'PR #'), 30 | } 31 | # on_rtd is whether we are on readthedocs.org 32 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 33 | 34 | if not on_rtd: # only set the theme if we're building docs locally 35 | html_theme = 'sphinx_rtd_theme' 36 | 37 | html_use_smartypants = True 38 | html_last_updated_fmt = '%b %d, %Y' 39 | html_split_index = False 40 | html_sidebars = { 41 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 42 | } 43 | html_short_title = '%s-%s' % (project, version) 44 | 45 | napoleon_use_ivar = True 46 | napoleon_use_rtype = False 47 | napoleon_use_param = False 48 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/pyramid.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | 5 | def build_gaussian_pyramid(src, level=3): 6 | s = src.copy() 7 | pyramid = [s] 8 | for i in range(level): 9 | s = cv2.pyrDown(s) 10 | pyramid.append(s) 11 | return pyramid 12 | 13 | 14 | def build_laplacian_pyramid(src, levels=3): 15 | gaussianPyramid = build_gaussian_pyramid(src, levels) 16 | pyramid = [] 17 | for i in range(levels, 0, -1): 18 | GE = cv2.pyrUp(gaussianPyramid[i]) 19 | L = cv2.subtract(gaussianPyramid[i - 1], GE) 20 | pyramid.append(L) 21 | return pyramid 22 | 23 | 24 | def gaussian_video(video_tensor, levels=3): 25 | for i in range(0, video_tensor.shape[0]): 26 | frame = video_tensor[i] 27 | pyr = build_gaussian_pyramid(frame, level=levels) 28 | gaussian_frame = pyr[-1] 29 | if i == 0: 30 | vid_data = np.zeros((video_tensor.shape[0], gaussian_frame.shape[0], gaussian_frame.shape[1], 3)) 31 | vid_data[i] = gaussian_frame 32 | return vid_data 33 | 34 | 35 | def laplacian_video(video_tensor, levels=3): 36 | tensor_list = [] 37 | for i in range(0, video_tensor.shape[0]): 38 | frame = video_tensor[i] 39 | pyr = build_laplacian_pyramid(frame, levels=levels) 40 | if i == 0: 41 | for k in range(levels): 42 | tensor_list.append(np.zeros((video_tensor.shape[0], pyr[k].shape[0], pyr[k].shape[1], 3))) 43 | for n in range(levels): 44 | tensor_list[n][i] = pyr[n] 45 | return tensor_list 46 | -------------------------------------------------------------------------------- /tests/test_magnify_color.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy import testing 3 | 4 | from python_eulerian_video_magnification.magnifycolor import MagnifyColor 5 | from python_eulerian_video_magnification.metadata import MetaData 6 | from python_eulerian_video_magnification.mode import Mode 7 | 8 | 9 | def test_reconstruct_video(): 10 | original = np.ones((32, 200, 300, 3), dtype='float') 11 | modified = np.ones((32, 100, 150, 3), dtype='float') 12 | # the pyramid up with only 1 level will double the dimensions of modified exactly one time 13 | 14 | meta = MetaData( 15 | file_name="/fu/bar/gob.avi", 16 | output_folder="/out/put/", 17 | mode=Mode.COLOR, 18 | suffix="color", 19 | low=0.1, 20 | high=2.1, 21 | levels=1, 22 | amplification=23 23 | ) 24 | magnify = MagnifyColor(data=meta) 25 | 26 | final = magnify._reconstruct_video(modified, original) 27 | testing.assert_array_equal(final, np.full((32, 200, 300, 3), 2)) 28 | 29 | 30 | def test_for_pyrup_rounding_error_fix(): 31 | image = np.ones((100, 150, 3), dtype='float') 32 | 33 | meta = MetaData( 34 | file_name="/fu/bar/gob.avi", 35 | output_folder="/out/put/", 36 | mode=Mode.COLOR, 37 | suffix="color", 38 | low=0.1, 39 | high=2.1, 40 | levels=1, 41 | amplification=23 42 | ) 43 | magnify = MagnifyColor(data=meta) 44 | 45 | dimensions = (200, 300, 3) 46 | solution = magnify._correct_dimensionality_problem_after_pyr_up(image, dimensions) 47 | testing.assert_equal(dimensions, solution.shape) 48 | -------------------------------------------------------------------------------- /ci/templates/.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | environment: 4 | global: 5 | COVERALLS_EXTRAS: '-v' 6 | COVERALLS_REPO_TOKEN: CmhNs4cpmc7aNcp5FPB4sf379OiXLhufo 7 | matrix: 8 | - TOXENV: check 9 | TOXPYTHON: C:\Python36\python.exe 10 | PYTHON_HOME: C:\Python36 11 | PYTHON_VERSION: '3.6' 12 | PYTHON_ARCH: '32' 13 | {% for env in tox_environments %} 14 | {% if env.startswith(('py2', 'py3')) %} 15 | - TOXENV: {{ env }},codecov,coveralls{{ "" }} 16 | TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe 17 | PYTHON_HOME: C:\Python{{ env[2:4] }} 18 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 19 | PYTHON_ARCH: '32' 20 | {% if 'nocov' in env %} 21 | WHEEL_PATH: .tox/dist 22 | {% endif %} 23 | - TOXENV: {{ env }},codecov,coveralls{{ "" }} 24 | TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe 25 | PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 26 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 27 | PYTHON_ARCH: '64' 28 | {% if 'nocov' in env %} 29 | WHEEL_PATH: .tox/dist 30 | {% endif %} 31 | {% if env.startswith('py2') %} 32 | WINDOWS_SDK_VERSION: v7.0 33 | {% endif %} 34 | {% endif %}{% endfor %} 35 | init: 36 | - ps: echo $env:TOXENV 37 | - ps: ls C:\Python* 38 | install: 39 | - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' 40 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 41 | - '%PYTHON_HOME%\Scripts\easy_install --version' 42 | - '%PYTHON_HOME%\Scripts\pip --version' 43 | - '%PYTHON_HOME%\Scripts\tox --version' 44 | test_script: 45 | - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox 46 | on_failure: 47 | - ps: dir "env:" 48 | - ps: get-content .tox\*\log\* 49 | 50 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 51 | # on_finish: 52 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 53 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/metadata.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | from datetime import datetime 4 | 5 | from python_eulerian_video_magnification.mode import Mode 6 | 7 | 8 | class MetaData: 9 | 10 | def __init__(self, file_name: str, output_folder: str, 11 | mode: Mode, suffix: str, 12 | low: float, high: float, 13 | levels: int = 3, amplification: int = 20 14 | ): 15 | self.__data = { 16 | 'file': file_name, 17 | 'output': output_folder, 18 | 'target': MetaData.output_file_name(file_name, suffix=suffix, path=output_folder), 19 | 'meta_target': MetaData.output_file_name(file_name, suffix=suffix, path=output_folder, generate_json=True), 20 | 'low': low, 21 | 'high': high, 22 | 'levels': levels, 23 | 'amplification': amplification, 24 | 'mode': mode.name, 25 | 'suffix': suffix, 26 | 'date': None, 27 | } 28 | 29 | @staticmethod 30 | def output_file_name(filename: str, suffix: str, path: str, generate_json=False): 31 | filename_split = os.path.splitext(os.path.split(filename)[-1]) 32 | extension = ".json" if generate_json else filename_split[1] 33 | return os.path.join(path, filename_split[0] + "_%s_evm_%s" % 34 | (suffix, MetaData.format_date(MetaData.get_date())) + extension) 35 | 36 | def __getitem__(self, item): 37 | return self.__data[item] 38 | 39 | def save_meta_data(self): 40 | """stores the meta data dictionary as a json""" 41 | self.__data['date'] = MetaData.format_date(MetaData.get_date()) 42 | with open(self.__data['meta_target'], 'w') as fp: 43 | json.dump(self.__data, fp=fp, sort_keys=True, indent=4, separators=(',', ': ')) 44 | 45 | @staticmethod 46 | def get_date() -> datetime: 47 | return datetime.now() 48 | 49 | @staticmethod 50 | def format_date(date: datetime) -> str: 51 | return date.strftime("%Y-%m-%d-%H-%M-%S") 52 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv:bootstrap] 2 | deps = 3 | jinja2 4 | matrix 5 | tox 6 | skip_install = true 7 | commands = 8 | python ci/bootstrap.py --no-env 9 | passenv = 10 | * 11 | ; a generative tox configuration, see: https://tox.readthedocs.io/en/latest/config.html#generative-envlist 12 | 13 | [tox] 14 | envlist = 15 | clean, 16 | check, 17 | docs, 18 | {py36,py37,py38}, 19 | report 20 | ignore_basepython_conflict = true 21 | 22 | [testenv] 23 | basepython = 24 | py36: {env:TOXPYTHON:python3.6} 25 | {py37,docs}: {env:TOXPYTHON:python3.7} 26 | py38: {env:TOXPYTHON:python3.8} 27 | {bootstrap,clean,check,report,codecov,coveralls}: {env:TOXPYTHON:python3} 28 | setenv = 29 | PYTHONPATH={toxinidir}/tests 30 | PYTHONUNBUFFERED=yes 31 | passenv = 32 | * 33 | usedevelop = false 34 | deps = 35 | pytest 36 | pytest-travis-fold 37 | pytest-cov 38 | commands = 39 | {posargs:pytest --cov --cov-report=term-missing -vv tests} 40 | 41 | [testenv:check] 42 | deps = 43 | twine 44 | wheel 45 | check-manifest 46 | flake8 47 | readme-renderer 48 | pygments 49 | isort 50 | skip_install = true 51 | commands = 52 | python setup.py bdist_wheel sdist 53 | twine check dist/*.whl dist/*.tar.gz 54 | check-manifest {toxinidir} 55 | flake8 src tests setup.py 56 | isort --verbose --check-only --diff --recursive src tests setup.py 57 | 58 | [testenv:docs] 59 | usedevelop = true 60 | deps = 61 | -r{toxinidir}/docs/requirements.txt 62 | commands = 63 | sphinx-build {posargs:-E} -b html docs dist/docs 64 | sphinx-build -b linkcheck docs dist/docs 65 | 66 | [testenv:coveralls] 67 | deps = 68 | coveralls 69 | skip_install = true 70 | commands = 71 | coveralls [] 72 | 73 | [testenv:codecov] 74 | deps = 75 | codecov 76 | skip_install = true 77 | commands = 78 | codecov [] 79 | 80 | [testenv:report] 81 | deps = coverage 82 | skip_install = true 83 | commands = 84 | coverage report 85 | coverage html 86 | 87 | [testenv:clean] 88 | commands = coverage erase 89 | skip_install = true 90 | deps = coverage 91 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | import os.path 4 | 5 | import pytest 6 | 7 | from python_eulerian_video_magnification import cli 8 | from python_eulerian_video_magnification.mode import Mode 9 | 10 | base_path = functools.partial(os.path.join, os.getcwd(), 'tests', 'videos') 11 | sample = base_path('guitar1sec.mp4') 12 | wrong_video = base_path('wrong_video.txt') 13 | 14 | 15 | def test_for_correctly_set_file(): 16 | sut = cli.CLI() 17 | sut.parse(args=[sample]) 18 | assert sut.get_file.name == sample 19 | 20 | 21 | def test_for_wrong_file(): 22 | sut = cli.CLI() 23 | with pytest.raises(SystemExit) as e: 24 | sut.parse(args=[wrong_video]) 25 | assert "10" in str(e.value) 26 | 27 | 28 | def test_check_mode(): 29 | sut = cli.CLI() 30 | sut.parse(args=[sample, '-m', 'motion']) 31 | assert sut.get_mode == Mode.MOTION 32 | 33 | 34 | def test_mode_default(): 35 | sut = cli.CLI() 36 | sut.parse(args=[sample]) 37 | assert sut.get_mode == Mode.COLOR 38 | 39 | 40 | def test_log_level_set(): 41 | sut = cli.CLI() 42 | sut.parse(args=[sample, '--loglevel=warning']) 43 | assert sut.get_log_level == logging.WARNING, "log level should be warning" 44 | 45 | 46 | def test_log_level_wrong(): 47 | sut = cli.CLI() 48 | with pytest.raises(SystemExit) as e: 49 | sut.parse(args=[sample, '--loglevel=fubar']) 50 | assert "2" in str(e.value) 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "value, parameter_flag, method", 55 | [ 56 | (0.5, "-c", 'get_low'), 57 | (3.2, '-p', 'get_high'), 58 | (5, '-l', 'get_levels'), 59 | (10, '-a', 'get_amplification') 60 | ] 61 | ) 62 | def test_correctly_set_numeric(value, parameter_flag, method): 63 | sut = cli.CLI() 64 | sut.parse(args=[sample, '%s %s' % (parameter_flag, value)]) 65 | assert getattr(sut, method) == value 66 | 67 | 68 | def test_for_invalid_output_path(tmp_path): 69 | sut = cli.CLI() 70 | path = os.path.join(tmp_path, 'not_existent.path') 71 | assert not os.path.exists(path) 72 | sut.parse(args=[sample, '-o', '%s' % path]) 73 | assert os.path.exists(path) 74 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/magnifycolor.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | from python_eulerian_video_magnification.filter import temporal_ideal_filter 5 | from python_eulerian_video_magnification.magnify import Magnify 6 | from python_eulerian_video_magnification.pyramid import gaussian_video 7 | 8 | 9 | class MagnifyColor(Magnify): 10 | def _magnify_impl(self, tensor: np.ndarray, fps: int) -> np.ndarray: 11 | gau_video = gaussian_video(tensor, levels=self._levels) 12 | filtered_tensor = temporal_ideal_filter(gau_video, self._low, self._high, fps) 13 | amplified_video = self._amplify_video(filtered_tensor) 14 | return self._reconstruct_video(amplified_video, tensor) 15 | 16 | def _amplify_video(self, gaussian_vid): 17 | return gaussian_vid * self._amplification 18 | 19 | def _reconstruct_video(self, amp_video, origin_video): 20 | origin_video_shape = origin_video.shape[1:] 21 | for i in range(0, amp_video.shape[0]): 22 | img = amp_video[i] 23 | for x in range(self._levels): 24 | img = cv2.pyrUp(img) # this doubles the dimensions of img each time 25 | # ensure that dimensions are equal 26 | origin_video[i] += self._correct_dimensionality_problem_after_pyr_up(img, origin_video_shape) 27 | return origin_video 28 | 29 | def _correct_dimensionality_problem_after_pyr_up(self, img: np.ndarray, origin_video_frame_shape) -> np.ndarray: 30 | if img.shape != origin_video_frame_shape: 31 | return np.resize(img, origin_video_frame_shape) 32 | else: 33 | return img 34 | 35 | def principal_component_analysis(self, tensor: np.ndarray): 36 | # Data matrix tensor, assumes 0-centered 37 | n, m = tensor.shape 38 | assert np.allclose(tensor.mean(axis=0), np.zeros(m)) 39 | # Compute covariance matrix 40 | covariance_matrix = np.dot(tensor.T, tensor) / (n - 1) 41 | # Eigen decomposition 42 | eigen_vals, eigen_vecs = np.linalg.eig(covariance_matrix) 43 | # Project tensor onto PC space 44 | X_pca = np.dot(tensor, eigen_vecs) 45 | return X_pca 46 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | environment: 4 | global: 5 | COVERALLS_EXTRAS: '-v' 6 | COVERALLS_REPO_TOKEN: CmhNs4cpmc7aNcp5FPB4sf379OiXLhufo 7 | matrix: 8 | - TOXENV: check 9 | TOXPYTHON: C:\Python36\python.exe 10 | PYTHON_HOME: C:\Python36 11 | PYTHON_VERSION: '3.6' 12 | PYTHON_ARCH: '32' 13 | - TOXENV: py36,codecov,coveralls 14 | TOXPYTHON: C:\Python36\python.exe 15 | PYTHON_HOME: C:\Python36 16 | PYTHON_VERSION: '3.6' 17 | PYTHON_ARCH: '32' 18 | - TOXENV: py36,codecov,coveralls 19 | TOXPYTHON: C:\Python36-x64\python.exe 20 | PYTHON_HOME: C:\Python36-x64 21 | PYTHON_VERSION: '3.6' 22 | PYTHON_ARCH: '64' 23 | - TOXENV: py37,codecov,coveralls 24 | TOXPYTHON: C:\Python37\python.exe 25 | PYTHON_HOME: C:\Python37 26 | PYTHON_VERSION: '3.7' 27 | PYTHON_ARCH: '32' 28 | - TOXENV: py37,codecov,coveralls 29 | TOXPYTHON: C:\Python37-x64\python.exe 30 | PYTHON_HOME: C:\Python37-x64 31 | PYTHON_VERSION: '3.7' 32 | PYTHON_ARCH: '64' 33 | - TOXENV: py38,codecov,coveralls 34 | TOXPYTHON: C:\Python38\python.exe 35 | PYTHON_HOME: C:\Python38 36 | PYTHON_VERSION: '3.8' 37 | PYTHON_ARCH: '32' 38 | - TOXENV: py38,codecov,coveralls 39 | TOXPYTHON: C:\Python38-x64\python.exe 40 | PYTHON_HOME: C:\Python38-x64 41 | PYTHON_VERSION: '3.8' 42 | PYTHON_ARCH: '64' 43 | init: 44 | - ps: echo $env:TOXENV 45 | - ps: ls C:\Python* 46 | install: 47 | - '%PYTHON_HOME%\python -mpip install --progress-bar=off tox -rci/requirements.txt' 48 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 49 | - '%PYTHON_HOME%\Scripts\easy_install --version' 50 | - '%PYTHON_HOME%\Scripts\pip --version' 51 | - '%PYTHON_HOME%\Scripts\tox --version' 52 | test_script: 53 | - cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd %PYTHON_HOME%\Scripts\tox 54 | on_failure: 55 | - ps: dir "env:" 56 | - ps: get-content .tox\*\log\* 57 | 58 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 59 | # on_finish: 60 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 61 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/magnify.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | from python_eulerian_video_magnification.metadata import MetaData 5 | 6 | 7 | class Magnify: 8 | def __init__(self, data: MetaData): 9 | self._data = data 10 | 11 | def load_video(self) -> (np.ndarray, int): 12 | cap = cv2.VideoCapture(self._in_file_name) 13 | frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 14 | width, height = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 15 | fps = int(cap.get(cv2.CAP_PROP_FPS)) 16 | video_tensor = np.zeros((frame_count, height, width, 3), dtype='float') 17 | x = 0 18 | while cap.isOpened(): 19 | ret, frame = cap.read() 20 | if ret is True: 21 | video_tensor[x] = frame 22 | x += 1 23 | else: 24 | break 25 | return video_tensor, fps 26 | 27 | def save_video(self, video_tensor: np.ndarray) -> None: 28 | # four_cc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') 29 | four_cc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') 30 | [height, width] = video_tensor[0].shape[0:2] 31 | writer = cv2.VideoWriter(self._out_file_name, four_cc, 30, (width, height), 1) 32 | for i in range(0, video_tensor.shape[0]): 33 | writer.write(cv2.convertScaleAbs(video_tensor[i])) 34 | writer.release() 35 | 36 | def do_magnify(self) -> None: 37 | tensor, fps = self.load_video() 38 | video_tensor = self._magnify_impl(tensor, fps) 39 | self.save_video(video_tensor) 40 | self._data.save_meta_data() 41 | 42 | def _magnify_impl(self, tensor: np.ndarray, fps: int) -> np.ndarray: 43 | raise NotImplementedError("This should be overwritten!") 44 | 45 | @property 46 | def _low(self) -> float: 47 | return self._data['low'] 48 | 49 | @property 50 | def _high(self) -> float: 51 | return self._data['high'] 52 | 53 | @property 54 | def _levels(self) -> int: 55 | return self._data['levels'] 56 | 57 | @property 58 | def _amplification(self) -> float: 59 | return self._data['amplification'] 60 | 61 | @property 62 | def _in_file_name(self) -> str: 63 | return self._data['file'] 64 | 65 | @property 66 | def _out_file_name(self) -> str: 67 | return self._data['target'] 68 | -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from datetime import datetime 3 | 4 | from python_eulerian_video_magnification.metadata import MetaData 5 | from python_eulerian_video_magnification.mode import Mode 6 | 7 | 8 | def test_output_filename(monkeypatch): 9 | monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, 10 | microsecond=1)) 11 | out = MetaData.output_file_name("abd.avi", "color", "/fu/bar/") 12 | assert out == "/fu/bar/abd_color_evm_2020-01-01-16-16-16.avi" 13 | 14 | 15 | def test_output_filename_with_path(monkeypatch): 16 | monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, 17 | microsecond=1)) 18 | out = MetaData.output_file_name("/tmp/abd.avi", "color", "/fu/bar/") 19 | assert out == "/fu/bar/abd_color_evm_2020-01-01-16-16-16.avi" 20 | 21 | 22 | def test_output_filename_with_path_for_metadata(monkeypatch): 23 | monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, 24 | microsecond=1)) 25 | out = MetaData.output_file_name("/tmp/abd.avi", "color", "/fu/bar/", generate_json=True) 26 | assert out == "/fu/bar/abd_color_evm_2020-01-01-16-16-16.json" 27 | 28 | 29 | def test_for_data_as_expected(monkeypatch): 30 | monkeypatch.setattr(MetaData, "get_date", lambda: datetime(year=2020, month=1, day=1, hour=16, minute=16, second=16, 31 | microsecond=1)) 32 | sut = MetaData( 33 | file_name="/fu/bar/gob.avi", 34 | output_folder="/out/put/", 35 | mode=Mode.COLOR, 36 | suffix="color", 37 | low=0.1, 38 | high=2.1, 39 | levels=4, 40 | amplification=23 41 | ) 42 | assert getattr(sut, "_MetaData__data") == {'file': "/fu/bar/gob.avi", 'output': "/out/put/", 43 | 'target': "/out/put/gob_color_evm_2020-01-01-16-16-16.avi", 44 | 'meta_target': "/out/put/gob_color_evm_2020-01-01-16-16-16.json", 45 | 'low': 0.1, 'high': 2.1, 46 | 'levels': 4, 47 | 'amplification': 23, 'mode': 'COLOR', 'suffix': 'color', 'date': None} 48 | 49 | 50 | def test_json_save(tmp_path): 51 | sut = MetaData( 52 | file_name="/fu/bar/gob.avi", 53 | output_folder=str(tmp_path), 54 | mode=Mode.COLOR, 55 | suffix="color", 56 | low=0.1, 57 | high=2.1, 58 | levels=4, 59 | amplification=23 60 | ) 61 | sut.save_meta_data() 62 | assert os.path.exists(sut['meta_target']) 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | Python Eulerian Video Magnification could always use more documentation, whether as part of the 21 | official Python Eulerian Video Magnification docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/vgoehler/PyEVM/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `PyEVM` for local development: 39 | 40 | 1. Fork `PyEVM `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:vgoehler/PyEVM.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes run all the checks and docs builder with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``) [1]_. 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 77 | `run the tests `_ for each change you add in the pull request. 78 | 79 | It will be slower though ... 80 | 81 | Tips 82 | ---- 83 | 84 | To run a subset of tests:: 85 | 86 | tox -e envname -- pytest -k test_myfeature 87 | 88 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 89 | 90 | detox 91 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path 9 | # 10 | # See: 11 | # https://pypi.org/project/cookiepatcher 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | _extensions: ['jinja2_time.TimeExtension'] 20 | _template: 'gh:ionelmc/cookiecutter-pylibrary' 21 | allow_tests_inside_package: 'no' 22 | appveyor: 'yes' 23 | c_extension_function: 'longest' 24 | c_extension_module: '_python_eulerian_video_magnification' 25 | c_extension_optional: 'no' 26 | c_extension_support: 'no' 27 | c_extension_test_pypi: 'no' 28 | c_extension_test_pypi_username: 'vgoehler' 29 | codacy: 'no' 30 | codacy_projectid: '[Get ID from https://app.codacy.com/app/vgoehler/PyEVM/settings]' 31 | codeclimate: 'no' 32 | codecov: 'yes' 33 | command_line_interface: 'argparse' 34 | command_line_interface_bin_name: 'EVM' 35 | coveralls: 'yes' 36 | coveralls_token: 'CmhNs4cpmc7aNcp5FPB4sf379OiXLhufo' 37 | distribution_name: 'PyEVM' 38 | email: 'volker.goehler@informatik.tu-freiberg.de' 39 | full_name: 'Volker G Göhler' 40 | landscape: 'no' 41 | license: 'BSD 2-Clause License' 42 | linter: 'flake8' 43 | package_name: 'python_eulerian_video_magnification' 44 | project_name: 'Python Eulerian Video Magnification' 45 | project_short_description: 'Eulerian Video Magnification for Python' 46 | pypi_badge: 'no' 47 | pypi_disable_upload: 'yes' 48 | release_date: 'today' 49 | repo_hosting: 'github.com' 50 | repo_hosting_domain: 'github.com' 51 | repo_name: 'PyEVM' 52 | repo_username: 'vgoehler' 53 | requiresio: 'yes' 54 | scrutinizer: 'no' 55 | setup_py_uses_setuptools_scm: 'no' 56 | setup_py_uses_test_runner: 'no' 57 | sphinx_docs: 'yes' 58 | sphinx_docs_hosting: 'https://PyEVM.readthedocs.io/' 59 | sphinx_doctest: 'no' 60 | sphinx_theme: 'sphinx-rtd-theme' 61 | test_matrix_configurator: 'no' 62 | test_matrix_separate_coverage: 'no' 63 | test_runner: 'pytest' 64 | travis: 'yes' 65 | travis_osx: 'no' 66 | version: '0.1.0' 67 | website: 'https://github.com/vgoehler' 68 | year_from: '2019' 69 | year_to: '2020' 70 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | from os.path import abspath 11 | from os.path import dirname 12 | from os.path import exists 13 | from os.path import join 14 | 15 | base_path = dirname(dirname(abspath(__file__))) 16 | 17 | 18 | def check_call(args): 19 | print("+", *args) 20 | subprocess.check_call(args) 21 | 22 | 23 | def exec_in_env(): 24 | env_path = join(base_path, ".tox", "bootstrap") 25 | if sys.platform == "win32": 26 | bin_path = join(env_path, "Scripts") 27 | else: 28 | bin_path = join(env_path, "bin") 29 | if not exists(env_path): 30 | import subprocess 31 | 32 | print("Making bootstrap env in: {0} ...".format(env_path)) 33 | try: 34 | check_call([sys.executable, "-m", "venv", env_path]) 35 | except subprocess.CalledProcessError: 36 | try: 37 | check_call([sys.executable, "-m", "virtualenv", env_path]) 38 | except subprocess.CalledProcessError: 39 | check_call(["virtualenv", env_path]) 40 | print("Installing `jinja2` into bootstrap environment...") 41 | check_call([join(bin_path, "pip"), "install", "jinja2", "tox"]) 42 | python_executable = join(bin_path, "python") 43 | if not os.path.exists(python_executable): 44 | python_executable += '.exe' 45 | 46 | print("Re-executing with: {0}".format(python_executable)) 47 | print("+ exec", python_executable, __file__, "--no-env") 48 | os.execv(python_executable, [python_executable, __file__, "--no-env"]) 49 | 50 | def main(): 51 | import jinja2 52 | 53 | print("Project path: {0}".format(base_path)) 54 | 55 | jinja = jinja2.Environment( 56 | loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), 57 | trim_blocks=True, 58 | lstrip_blocks=True, 59 | keep_trailing_newline=True 60 | ) 61 | 62 | tox_environments = [ 63 | line.strip() 64 | # 'tox' need not be installed globally, but must be importable 65 | # by the Python that is running this script. 66 | # This uses sys.executable the same way that the call in 67 | # cookiecutter-pylibrary/hooks/post_gen_project.py 68 | # invokes this bootstrap.py itself. 69 | for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() 70 | ] 71 | tox_environments = [line for line in tox_environments if line.startswith('py')] 72 | 73 | for name in os.listdir(join("ci", "templates")): 74 | with open(join(base_path, name), "w") as fh: 75 | fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) 76 | print("Wrote {}".format(name)) 77 | print("DONE.") 78 | 79 | 80 | if __name__ == "__main__": 81 | args = sys.argv[1:] 82 | if args == ["--no-env"]: 83 | main() 84 | elif not args: 85 | exec_in_env() 86 | else: 87 | print("Unexpected arguments {0}".format(args), file=sys.stderr) 88 | sys.exit(1) 89 | 90 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import io 7 | import re 8 | from glob import glob 9 | from os.path import basename 10 | from os.path import dirname 11 | from os.path import join 12 | from os.path import splitext 13 | 14 | from setuptools import find_packages 15 | from setuptools import setup 16 | 17 | 18 | def read(*names, **kwargs): 19 | with io.open( 20 | join(dirname(__file__), *names), 21 | encoding=kwargs.get('encoding', 'utf8') 22 | ) as fh: 23 | return fh.read() 24 | 25 | 26 | setup( 27 | name='PyEVM', 28 | version='0.4.2', 29 | license='BSD-2-Clause', 30 | description='Eulerian Video Magnification for Python', 31 | long_description='%s\n%s' % ( 32 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 33 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 34 | ), 35 | author='Volker G Göhler', 36 | author_email='volker.goehler@informatik.tu-freiberg.de', 37 | url='https://github.com/vgoehler/PyEVM', 38 | packages=find_packages('src'), 39 | package_dir={'': 'src'}, 40 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 41 | include_package_data=True, 42 | zip_safe=False, 43 | classifiers=[ 44 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 45 | 'Development Status :: 4 - Beta', 46 | 'Intended Audience :: Developers', 47 | 'License :: OSI Approved :: BSD License', 48 | 'Operating System :: Unix', 49 | 'Operating System :: POSIX', 50 | 'Operating System :: Microsoft :: Windows', 51 | 'Programming Language :: Python', 52 | # 'Programming Language :: Python :: 2.7', 53 | 'Programming Language :: Python :: 3', 54 | # 'Programming Language :: Python :: 3.5', 55 | 'Programming Language :: Python :: 3.6', 56 | 'Programming Language :: Python :: 3.7', 57 | 'Programming Language :: Python :: 3.8', 58 | 'Programming Language :: Python :: Implementation :: CPython', 59 | # 'Programming Language :: Python :: Implementation :: PyPy', 60 | # uncomment if you test on these interpreters: 61 | # 'Programming Language :: Python :: Implementation :: IronPython', 62 | # 'Programming Language :: Python :: Implementation :: Jython', 63 | # 'Programming Language :: Python :: Implementation :: Stackless', 64 | 'Topic :: Utilities', 65 | 'Private :: Do Not Upload', 66 | ], 67 | project_urls={ 68 | 'Documentation': 'https://PyEVM.readthedocs.io/', 69 | 'Changelog': 'https://PyEVM.readthedocs.io/en/latest/changelog.html', 70 | 'Issue Tracker': 'https://github.com/vgoehler/PyEVM/issues', 71 | }, 72 | keywords=[ 73 | # eg: 'keyword1', 'keyword2', 'keyword3', 74 | ], 75 | python_requires='>=3.6', 76 | install_requires=[ 77 | # eg: 'aspectlib==1.1.1', 'six>=1.7', 78 | 'numpy>=1.18.0', 79 | 'opencv-python>=4.1.2.30', 80 | 'scipy>=1.4.1' 81 | ], 82 | extras_require={ 83 | # eg: 84 | # 'rst': ['docutils>=0.11'], 85 | # ':python_version=="2.6"': ['argparse'], 86 | }, 87 | entry_points={ 88 | 'console_scripts': [ 89 | 'EVM = python_eulerian_video_magnification.cli:main', 90 | ] 91 | }, 92 | ) 93 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | .. start-badges 6 | 7 | .. list-table:: 8 | :stub-columns: 1 9 | 10 | * - docs 11 | - |docs| 12 | * - tests 13 | - | |travis| |appveyor| |requires| 14 | | |coveralls| |codecov| 15 | * - package 16 | - | |commits-since| 17 | .. |docs| image:: https://readthedocs.org/projects/pyevm/badge/?style=flat 18 | :target: https://readthedocs.org/projects/pyevm 19 | :alt: Documentation Status 20 | 21 | .. |travis| image:: https://api.travis-ci.com/vgoehler/PyEVM.svg?branch=master 22 | :alt: Travis-CI Build Status 23 | :target: https://travis-ci.com/vgoehler/PyEVM 24 | 25 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/vgoehler/PyEVM?branch=master&svg=true 26 | :alt: AppVeyor Build Status 27 | :target: https://ci.appveyor.com/project/vgoehler/PyEVM 28 | 29 | .. |requires| image:: https://requires.io/github/vgoehler/PyEVM/requirements.svg?branch=master 30 | :alt: Requirements Status 31 | :target: https://requires.io/github/vgoehler/PyEVM/requirements/?branch=master 32 | 33 | .. |coveralls| image:: https://coveralls.io/repos/vgoehler/PyEVM/badge.svg?branch=master&service=github 34 | :alt: Coverage Status 35 | :target: https://coveralls.io/github/vgoehler/PyEVM 36 | 37 | .. |codecov| image:: https://codecov.io/gh/vgoehler/PyEVM/branch/master/graphs/badge.svg?branch=master 38 | :alt: Coverage Status 39 | :target: https://codecov.io/github/vgoehler/PyEVM 40 | 41 | .. |commits-since| image:: https://img.shields.io/github/commits-since/vgoehler/PyEVM/v0.4.2.svg 42 | :alt: Commits since latest release 43 | :target: https://github.com/vgoehler/PyEVM/compare/v0.4.2...master 44 | 45 | 46 | 47 | .. end-badges 48 | 49 | Eulerian Video Magnification for Python 50 | 51 | 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/)). 52 | >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. 53 | 54 | This is a fork from [flyingzhao/PyEVM](https://github.com/flyingzhao/PyEVM) as a basis for own work. 55 | It now has an operational command line interface and is install able. 56 | 57 | 58 | 59 | * Free software: BSD 2-Clause License 60 | 61 | Installation 62 | ============ 63 | 64 | Up until now it is not available with PyPI, but if it will be you could use this code to install it. 65 | 66 | :: 67 | 68 | pip install PyEVM 69 | 70 | You can install the in-development version with:: 71 | 72 | pip install https://github.com/vgoehler/PyEVM/archive/master.zip 73 | 74 | needed libraries (that get automatically installed) are: 75 | 76 | - numpy (>=1.17.4) 77 | - opencv-python (>=4.1.2.30) 78 | - scipy (>=1.3.3) 79 | 80 | 81 | Running 82 | ======= 83 | 84 | Navigate to sources directory and use 85 | 86 | :: 87 | 88 | python3 -mpython_eulerian_video_magnification inputfile.video 89 | 90 | if you just want to execute the code. 91 | 92 | Usage 93 | ===== 94 | 95 | optional arguments: 96 | ================================================ ==================================================== 97 | ``-h, --help`` show this help message and exit 98 | ================================================ ==================================================== 99 | 100 | system arguments: 101 | ================================================ ==================================================== 102 | ``input`` the input video file to work on 103 | ``-o [O]`` output-folder 104 | ``--color_suffix [COLOR_SUFFIX]`` the suffix to use for color modified result files 105 | ``--motion_suffix [MOTION_SUFFIX]`` the suffix to use for motion modified result files 106 | ``--log {debug,info,warning,error,critical}`` log level 107 | ================================================ ==================================================== 108 | 109 | parameters: 110 | =================================================== ==================================================== 111 | ``-m {color,motion}`` mode 112 | ``-c LOW, --low LOW`` low parameter (creek) 113 | ``-p HIGH, --high HIGH`` high parameter (peek) 114 | ``-l LEVELS, --levels LEVELS`` levels parameter 115 | ``-a AMPLIFICATION, --amplification AMPLIFICATION`` amplification parameter 116 | =================================================== ==================================================== 117 | 118 | Documentation 119 | ============= 120 | 121 | 122 | https://PyEVM.readthedocs.io/ 123 | 124 | 125 | Development 126 | =========== 127 | 128 | To run all tests run:: 129 | 130 | tox 131 | 132 | Note, to combine the coverage data from all the tox environments run: 133 | 134 | .. list-table:: 135 | :widths: 10 90 136 | :stub-columns: 1 137 | 138 | - - Windows 139 | - :: 140 | 141 | set PYTEST_ADDOPTS=--cov-append 142 | tox 143 | 144 | - - Other 145 | - :: 146 | 147 | PYTEST_ADDOPTS=--cov-append tox 148 | -------------------------------------------------------------------------------- /src/python_eulerian_video_magnification/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that contains the command line app. 3 | 4 | Why does this file exist, and why not put this in __main__? 5 | 6 | You might be tempted to import things from __main__ later, but that will cause 7 | problems: the code will get executed twice: 8 | 9 | - When you run `python -mpython_eulerian_video_magnification` python will execute 10 | ``__main__.py`` as a script. That means there won't be any 11 | ``python_eulerian_video_magnification.__main__`` in ``sys.modules``. 12 | - When you import __main__ it will get executed again (as a module) because 13 | there's no ``python_eulerian_video_magnification.__main__`` in ``sys.modules``. 14 | 15 | Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration 16 | """ 17 | import argparse 18 | import logging 19 | import os.path 20 | import sys 21 | from typing import IO 22 | 23 | from python_eulerian_video_magnification.magnifycolor import MagnifyColor 24 | from python_eulerian_video_magnification.magnifymotion import MagnifyMotion 25 | from python_eulerian_video_magnification.metadata import MetaData 26 | from python_eulerian_video_magnification.mode import Mode 27 | 28 | 29 | class CLI: 30 | """The command line interface for evm""" 31 | 32 | def __init__(self): 33 | self.args = None 34 | self.parser = argparse.ArgumentParser( 35 | description='This starts eulerian video magnification on the command line', 36 | epilog='volker.goehler@informatik.tu-freiberg.de', 37 | prog=os.path.split(sys.argv[0])[-1] if '__main__' not in sys.argv[0] else 'eulerian_video_magnification ' 38 | ) 39 | # prog parameter is a fix if the program is called from module level 40 | # io group 41 | io_group = self.parser.add_argument_group("system arguments") 42 | io_group.add_argument('input', type=argparse.FileType('r'), help="the input video file to work on") 43 | # TODO meta information for each file worked on, orig filename, timestamp start and end, size, parameter count 44 | io_group.add_argument('-o', help='output-folder', nargs='?', default=os.path.join(os.path.curdir, "videos"), 45 | type=str) 46 | io_group.add_argument('--color_suffix', help='the suffix to use for color modified result files', nargs='?', 47 | type=str, default='color') 48 | io_group.add_argument('--motion_suffix', help='the suffix to use for motion modified result files', nargs='?', 49 | type=str, default='motion') 50 | io_group.add_argument('--loglevel', help="log level", choices=["debug", "info", "warning", "error", "critical"], 51 | type=str, 52 | default="warning") 53 | 54 | # arguments 55 | arg_group = self.parser.add_argument_group("parameters") 56 | arg_group.add_argument('-m', '--mode', help='the mode of the operation, either enhance colors or motions', 57 | type=Mode.from_string, 58 | choices=list(Mode), default=Mode.COLOR) 59 | arg_group.add_argument('-c', '--low', default=0.4, type=float, help="low parameter (creek)") 60 | arg_group.add_argument('-p', '--high', default=3, type=float, help="high parameter (peek)") 61 | arg_group.add_argument('-l', '--levels', default=3, type=int, help="levels parameter") 62 | arg_group.add_argument('-a', '--amplification', default=20, type=int, help="amplification parameter") 63 | 64 | def parse(self, args: list): 65 | self.args = self.parser.parse_args(args=args) 66 | self.__sanitize_input() 67 | 68 | def __sanitize_input(self): 69 | """ This checks for further conditions in input args """ 70 | self.__check_for_video_file() 71 | self.__manage_output_folder() 72 | 73 | def __check_for_video_file(self): 74 | """ we check if the input file is valid """ 75 | formats = ('avi', 'mpg', 'mpeg', 'mp4') 76 | if os.path.splitext(self.args.input.name)[-1] in (".%s" % ext for ext in formats): 77 | # we got a valid (at least according to extension) file 78 | pass 79 | else: 80 | logging.critical("Input is not a video file. Only supports %s" % ", ".join(formats)) 81 | sys.exit(10) 82 | 83 | def __manage_output_folder(self): 84 | """ in case the output folder is not existent we create it """ 85 | if not os.path.exists(self.output_folder): 86 | os.makedirs(self.output_folder) 87 | 88 | @property 89 | def get_log_level(self) -> int: 90 | """ parses the input loglevel to the numeric value """ 91 | logging.debug(self.args) 92 | return getattr(logging, self.args.loglevel.upper(), None) 93 | 94 | @property 95 | def get_mode(self) -> Mode: 96 | logging.debug(self.args) 97 | return self.args.mode 98 | 99 | @property 100 | def get_file(self) -> IO: 101 | return self.args.input 102 | 103 | @property 104 | def get_low(self) -> float: 105 | return self.args.low 106 | 107 | @property 108 | def get_high(self) -> float: 109 | return self.args.high 110 | 111 | @property 112 | def get_levels(self) -> int: 113 | return self.args.levels 114 | 115 | @property 116 | def get_amplification(self) -> int: 117 | return self.args.amplification 118 | 119 | @property 120 | def output_folder(self) -> str: 121 | return self.args.o 122 | 123 | 124 | def main(args=None): 125 | cli = CLI() 126 | cli.parse(args=args) 127 | 128 | logging.basicConfig(level=cli.get_log_level) 129 | 130 | # create magnification correct Object 131 | if cli.get_mode == Mode.COLOR: 132 | print("Starting Magnification in Color Mode") 133 | magnify = MagnifyColor 134 | suffix = cli.args.color_suffix 135 | elif cli.get_mode == Mode.MOTION: 136 | print("Starting Magnification in Motion Mode") 137 | magnify = MagnifyMotion 138 | suffix = cli.args.motion_suffix 139 | else: 140 | raise NotImplementedError("Unknown Mode") 141 | 142 | meta_data = MetaData( 143 | file_name=cli.get_file.name, 144 | output_folder=cli.args.o, 145 | mode=cli.get_mode, 146 | suffix=suffix, 147 | low=cli.get_low, 148 | high=cli.get_high, 149 | levels=cli.get_levels, 150 | amplification=cli.get_amplification 151 | ) 152 | 153 | work = magnify(meta_data) 154 | work.do_magnify() 155 | --------------------------------------------------------------------------------