From 0025a743631dce13106d781cb058c98f5234e1f7 Mon Sep 17 00:00:00 2001 From: Adam Theisen Date: Tue, 19 Dec 2023 12:52:23 -0600 Subject: [PATCH] Base functionality to create movies from images (#776) * ADD: Adding utility to combine images into a movie using moviepy * ENH: Adding moviepy to readme as optional dependency and to CI file * ENH: Adding moviepy to docs build environment as well * ENH: Resolving requests for modifications * ENH: PEP8 errors * ENH: Changing test to use images on github repo --- README.rst | 1 + act/tests/utils/test_io_utils.py | 23 +++++++- act/utils/__init__.py | 3 +- act/utils/io_utils.py | 58 +++++++++++++++++++ .../environment_actions.yml | 1 + docs/environment_docs.yml | 1 + 6 files changed, 85 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cf6dd3c1c3..b6536a7a77 100644 --- a/README.rst +++ b/README.rst @@ -90,6 +90,7 @@ Optional Dependencies * `scikit-posthocs `_ Using interquartile range or generalized Extreme Studentized Deviate quality control tests * `icartt `_ icartt is an ICARTT file format reader and writer for Python * `PySP2 `_ PySP2 is a python package for reading and processing Single Particle Soot Photometer (SP2) datasets. +* `MoviePy `_ MoviePy is a python package for creating movies from images Installation ~~~~~~~~~~~~ diff --git a/act/tests/utils/test_io_utils.py b/act/tests/utils/test_io_utils.py index ffe259974e..8f37ce44c4 100644 --- a/act/tests/utils/test_io_utils.py +++ b/act/tests/utils/test_io_utils.py @@ -1,12 +1,21 @@ import tempfile from pathlib import Path -from os import PathLike +from os import PathLike, getcwd from string import ascii_letters import random +import glob +import pytest +import os import act from act.tests import sample_files +try: + import moviepy.video.io.ImageSequenceClip + MOVIEPY_AVAILABLE = True +except ImportError: + MOVIEPY_AVAILABLE = False + def test_read_netcdf_gztarfiles(): with tempfile.TemporaryDirectory() as tmpdirname: @@ -203,3 +212,15 @@ def test_gunzip(): assert file.endswith('.nc') assert Path(unpack_filename).is_file() is False + + +@pytest.mark.skipif(not MOVIEPY_AVAILABLE, reason='MoviePy is not installed.') +def test_generate_movie(): + files = [ + 'https://github.com/ARM-DOE/ACT/blob/main/act/tests/plotting/baseline/test_contour.png?raw=true', + 'https://github.com/ARM-DOE/ACT/blob/main/act/tests/plotting/baseline/test_contour2.png?raw=true', + 'https://github.com/ARM-DOE/ACT/blob/main/act/tests/plotting/baseline/test_contourf.png?raw=true', + 'https://github.com/ARM-DOE/ACT/blob/main/act/tests/plotting/baseline/test_contourf2.png?raw=true' + ] + result = act.utils.generate_movie(files, fps=5) + assert str(result) == 'movie.mp4' diff --git a/act/utils/__init__.py b/act/utils/__init__.py index 867c0d8690..035bf99de3 100644 --- a/act/utils/__init__.py +++ b/act/utils/__init__.py @@ -49,7 +49,8 @@ 'cleanup_files', 'is_gunzip_file', 'pack_gzip', - 'unpack_gzip' + 'unpack_gzip', + 'generate_movie' ], }, ) diff --git a/act/utils/io_utils.py b/act/utils/io_utils.py index c7b4dcfe7a..8da106a19c 100644 --- a/act/utils/io_utils.py +++ b/act/utils/io_utils.py @@ -8,6 +8,12 @@ import shutil import tempfile +try: + import moviepy.video.io.ImageSequenceClip + MOVIEPY_AVAILABLE = True +except ImportError: + MOVIEPY_AVAILABLE = False + def pack_tar(filenames, write_filename=None, write_directory=None, remove=False): """ @@ -276,3 +282,55 @@ def unpack_gzip(filename, write_directory=None, remove=False): Path(filename).unlink() return str(write_filename) + + +def generate_movie(images, write_directory=None, write_filename=None, fps=10, codec=None, threads=None): + """ + Creates a movie from a list of images + + ... + + Parameters + ---------- + images : list + List of images in the correct order to make into a movie + write_directory : str, pahtlib.Path, list, None + Path to directory to place newly created gunzip file. + write_filename : str, pathlib.Path, None + Movie output filename + fps: int + Frames per second + codec: int + Codec to use for image encoding + threads: int + Number of threads to use for ffmpeg + + + Returns + ------- + write_filename : str + Full path name of created gunzip file + + """ + if not MOVIEPY_AVAILABLE: + raise ImportError( + 'MoviePy needs to be installed on your system to make movies.' + ) + + if write_filename is None: + write_filename = 'movie.mp4' + + if write_directory is not None: + write_directory = Path(write_directory) + write_directory.mkdir(parents=True, exist_ok=True) + write_filename = Path(write_filename).name + elif Path(write_filename).parent != Path('.'): + write_directory = Path(write_filename).parent + else: + write_directory = Path('.') + + full_path = write_directory / write_filename + clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip(images, fps=fps) + clip.write_videofile(str(full_path), codec=codec, threads=threads) + + return full_path diff --git a/continuous_integration/environment_actions.yml b/continuous_integration/environment_actions.yml index 900525e6a0..0443eb0502 100644 --- a/continuous_integration/environment_actions.yml +++ b/continuous_integration/environment_actions.yml @@ -28,6 +28,7 @@ dependencies: - lazy_loader - cmweather - arm-test-data + - moviepy - pip: - mpl2nc - metpy diff --git a/docs/environment_docs.yml b/docs/environment_docs.yml index 867230ad92..1effcf3da0 100644 --- a/docs/environment_docs.yml +++ b/docs/environment_docs.yml @@ -19,6 +19,7 @@ dependencies: - pip - shapely<1.8.3 - arm-test-data + - moviepy - pip: - mpl2nc - lazy_loader