diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d1f5aefc..f6725c4f 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -43,7 +43,7 @@ jobs: run: | curl -Os https://cli.codecov.io/latest/linux/codecov chmod +x codecov - ./codecov --verbose upload-process --fail-on-error -t ${{ secrets.CODECOV_TOKEN }} -n 'service'-${{ github.run_id }} -F service -f coverage.xml + ./codecov --verbose upload-process --fail-on-error -n 'service'-${{ github.run_id }} -F service -f coverage.xml test: needs: build diff --git a/tensorboardX/summary.py b/tensorboardX/summary.py index 549a03aa..f46c5529 100644 --- a/tensorboardX/summary.py +++ b/tensorboardX/summary.py @@ -1,4 +1,3 @@ - import logging import os import re as _re @@ -368,33 +367,56 @@ def video(tag, tensor, fps=4, dataformats="NTCHW"): def make_video(tensor, fps): + import tempfile + from importlib.metadata import PackageNotFoundError + from importlib.metadata import version as get_version + + from packaging.version import Version try: - import moviepy # noqa: F401 - except ImportError: - print('add_video needs package moviepy') + moviepy_version = Version(get_version("moviepy")) + except PackageNotFoundError: + logger.error("moviepy is not installed.") return + try: - from moviepy import editor as mpy + # moviepy v2+ + from moviepy import ImageSequenceClip except ImportError: - print("moviepy is installed, but can't import moviepy.editor.", - "Some packages could be missing [imageio, requests]") - return - - import tempfile - - import moviepy.version - + try: + # Fallback for all moviepy versions + from moviepy.video.io.ImageSequenceClip import ImageSequenceClip + except ImportError as e: + logger.error( + "Can't create video. moviepy is installed, but can't import moviepy.video.io.ImageSequenceClip due to %r", + e, + ) + return + + # Warn about potential moviepy and imageio version incompatibility + imageio_version = Version(get_version("imageio")) + if moviepy_version >= Version("2") and imageio_version < Version("2.29"): + logger.error( + "You are using moviepy >= 2.0.0 and imageio < 2.29.0. " + "This combination is known to cause issues when writing videos. " + "Please upgrade imageio to 2.29 or later, or use moviepy < 2.0.0." + ) t, h, w, c = tensor.shape + # Convert to RGB if moviepy v2/imageio>2.27 is used; 1-channel input is not supported. + if c == 1 and ( + moviepy_version >= Version("2") + or imageio_version > Version("2.27") + ): + tensor = np.repeat(tensor, 3, axis=-1) # encode sequence of images into gif string - clip = mpy.ImageSequenceClip(list(tensor), fps=fps) + clip = ImageSequenceClip(list(tensor), fps=fps) with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as fp: filename = fp.name - if moviepy.version.__version__.startswith("0."): + if moviepy_version < Version("1.0.0"): logger.warning('Upgrade to moviepy >= 1.0.0 to supress the progress bar.') clip.write_gif(filename, verbose=False) - elif moviepy.version.__version__.startswith("1."): + elif moviepy_version < Version("2.0.0dev1"): # moviepy >= 1.0.0 use logger=None to suppress output. clip.write_gif(filename, verbose=False, logger=None) else: diff --git a/tests/expect/test_summary.test_video.expect.gif b/tests/expect/test_summary.test_video.expect.gif new file mode 100644 index 00000000..0ba79c49 Binary files /dev/null and b/tests/expect/test_summary.test_video.expect.gif differ diff --git a/tests/test_summary.py b/tests/test_summary.py index 950f9bec..69a485e8 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -1,10 +1,14 @@ from __future__ import absolute_import, division, print_function, unicode_literals + +import io from tensorboardX import summary from .expect_reader import compare_proto, write_proto import numpy as np import pytest import unittest import torch + +from PIL import Image, ImageSequence # compare_proto = write_proto # massive update expect def tensor_N(shape, dtype=float): @@ -76,12 +80,40 @@ def test_image_with_four_channel_batched(self): def test_image_without_channel(self): compare_proto(summary.image('dummy', tensor_N(shape=(8, 8)), dataformats='HW'), self) + @staticmethod + def _iter_gif(encoded_image): + image_io = io.BytesIO(encoded_image) + im = Image.open(image_io, ) + for frame in ImageSequence.Iterator(im): + yield frame.getchannel(0) + + @staticmethod + def _load_expected_test_video(): + with Image.open("tests/expect/test_summary.test_video.expect.gif") as im: + return list(ImageSequence.Iterator(im)) + + def assert_grayscale(self, image) -> None: + channels = image.split() + c0colors = channels[0].getcolors() + for c in channels[1:]: + self.assertEqual(c0colors, c.getcolors()) + def test_video(self): try: import moviepy except ImportError: - return - compare_proto(summary.video('dummy', tensor_N(shape=(4, 3, 1, 8, 8))), self) + self.skipTest('moviepy not installed') + t1 = tensor_N(shape=(4, 3, 1, 8, 8)) + v1 = summary.video("dummy", t1) + frames = list(self._iter_gif(v1.value[0].image.encoded_image_string)) + self.assertEqual(len(frames), 3) + prepared = self._load_expected_test_video() + for image, expected in zip(frames, prepared): + self.assert_grayscale(image) + self.assert_grayscale(expected) + self.assertEqual( + image.getchannel(0).getcolors(), expected.getchannel(0).getcolors() + ) summary.video('dummy', tensor_N(shape=(16, 48, 1, 28, 28))) summary.video('dummy', tensor_N(shape=(20, 7, 1, 8, 8)))