From 493bafb689a90a97555e68fe42100625d1d6e7f9 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 19:34:56 +0800 Subject: [PATCH 01/14] [DLMED] add support for both TensorBoard and TensorBoardX Signed-off-by: Nic Ma --- monai/visualize/img2tensorboard.py | 50 ++++++++++++++++++++++++------ tests/test_plot_2d_or_3d_image.py | 14 +++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index fd6dc9483b..1f45cd33eb 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -24,15 +24,27 @@ if TYPE_CHECKING: from tensorboard.compat.proto.summary_pb2 import Summary from torch.utils.tensorboard import SummaryWriter + from tensorboardX.proto.summary_pb2 import Summary as SummaryX + from tensorboardX import SummaryWriter as SummaryWriterX + has_tensorboardX = True else: Summary, _ = optional_import("tensorboard.compat.proto.summary_pb2", name="Summary") SummaryWriter, _ = optional_import("torch.utils.tensorboard", name="SummaryWriter") + SummaryX, has_tensorboardX = optional_import("tensorboardX.proto.summary_pb2", name="Summary") + SummaryWriterX, has_tensorboardX = optional_import("tensorboardX", name="SummaryWriter") + + __all__ = ["make_animated_gif_summary", "add_animated_gif", "add_animated_gif_no_channels", "plot_2d_or_3d_image"] -def _image3_animated_gif(tag: str, image: Union[np.ndarray, torch.Tensor], scale_factor: float = 1.0) -> Summary: +def _image3_animated_gif( + tag: str, + image: Union[np.ndarray, torch.Tensor], + writer: Union[SummaryWriter, SummaryWriterX], + scale_factor: float = 1.0, +): """Function to actually create the animated gif. Args: @@ -54,14 +66,17 @@ def _image3_animated_gif(tag: str, image: Union[np.ndarray, torch.Tensor], scale for b_data in PIL.GifImagePlugin.getdata(i): img_str += b_data img_str += b"\x3B" - summary_image_str = Summary.Image(height=10, width=10, colorspace=1, encoded_image_string=img_str) - image_summary = Summary.Value(tag=tag, image=summary_image_str) - return Summary(value=[image_summary]) + + summary = SummaryX if has_tensorboardX and isinstance(writer, SummaryWriterX) else Summary + summary_image_str = summary.Image(height=10, width=10, colorspace=1, encoded_image_string=img_str) + image_summary = summary.Value(tag=tag, image=summary_image_str) + return summary(value=[image_summary]) def make_animated_gif_summary( tag: str, image: Union[np.ndarray, torch.Tensor], + writer: Union[SummaryWriter, SummaryWriterX], max_out: int = 3, animation_axes: Sequence[int] = (3,), image_axes: Sequence[int] = (1, 2), @@ -73,6 +88,7 @@ def make_animated_gif_summary( Args: tag: Data identifier image: The image, expected to be in CHWD format + writer: the tensorboard writer to plot image max_out: maximum number of slices to animate through animation_axes: axis to animate on (not currently used) image_axes: axes of image (not currently used) @@ -99,12 +115,12 @@ def make_animated_gif_summary( one_channel_img: Union[torch.Tensor, np.ndarray] = ( image[it_i, :, :, :].squeeze(dim=0) if isinstance(image, torch.Tensor) else image[it_i, :, :, :] ) - summary_op = _image3_animated_gif(tag + suffix.format(it_i), one_channel_img, scale_factor) + summary_op = _image3_animated_gif(tag + suffix.format(it_i), one_channel_img, writer, scale_factor) return summary_op def add_animated_gif( - writer: SummaryWriter, + writer: Union[SummaryWriter, SummaryWriterX], tag: str, image_tensor: Union[np.ndarray, torch.Tensor], max_out: int, @@ -124,7 +140,13 @@ def add_animated_gif( """ writer._get_file_writer().add_summary( make_animated_gif_summary( - tag, image_tensor, max_out=max_out, animation_axes=[1], image_axes=[2, 3], scale_factor=scale_factor + tag=tag, + image=image_tensor, + writer=writer, + max_out=max_out, + animation_axes=[1], + image_axes=[2, 3], + scale_factor=scale_factor, ), global_step, ) @@ -153,7 +175,13 @@ def add_animated_gif_no_channels( """ writer._get_file_writer().add_summary( make_animated_gif_summary( - tag, image_tensor, max_out=max_out, animation_axes=[1], image_axes=[1, 2], scale_factor=scale_factor + tag=tag, + image=image_tensor, + writer=writer, + max_out=max_out, + animation_axes=[1], + image_axes=[1, 2], + scale_factor=scale_factor, ), global_step, ) @@ -162,7 +190,7 @@ def add_animated_gif_no_channels( def plot_2d_or_3d_image( data: Union[NdarrayTensor, List[NdarrayTensor]], step: int, - writer: SummaryWriter, + writer: Union[SummaryWriter, SummaryWriterX], index: int = 0, max_channels: int = 1, max_frames: int = 64, @@ -206,7 +234,9 @@ def plot_2d_or_3d_image( if d.ndim >= 4: spatial = d.shape[-3:] - for j, d3 in enumerate(d.reshape([-1] + list(spatial))[:max_channels]): + d = d.reshape([-1] + list(spatial)) + + for j, d3 in enumerate(d[:max_channels]): d3 = rescale_array(d3, 0, 255) add_animated_gif(writer, f"{tag}_HWD_{j}", d3[None], max_frames, 1.0, step) return diff --git a/tests/test_plot_2d_or_3d_image.py b/tests/test_plot_2d_or_3d_image.py index 645658e311..8d980a2dd1 100644 --- a/tests/test_plot_2d_or_3d_image.py +++ b/tests/test_plot_2d_or_3d_image.py @@ -18,6 +18,10 @@ from torch.utils.tensorboard import SummaryWriter from monai.visualize import plot_2d_or_3d_image +from monai.utils import optional_import +from tests.utils import SkipIfNoModule + +SummaryWriterX, has_tensorboardX = optional_import("tensorboardX", name="SummaryWriter") TEST_CASE_1 = [(1, 1, 10, 10)] @@ -40,6 +44,16 @@ def test_tb_image_shape(self, shape): writer.close() self.assertTrue(len(glob.glob(tempdir)) > 0) + @SkipIfNoModule("tensorboardX") + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5]) + def test_tb_image_shape(self, shape): + with tempfile.TemporaryDirectory() as tempdir: + writer = SummaryWriterX(log_dir=tempdir) + plot_2d_or_3d_image(torch.zeros(shape), 0, writer) + writer.flush() + writer.close() + self.assertTrue(len(glob.glob(tempdir)) > 0) + if __name__ == "__main__": unittest.main() From 76892f06f509442d21caf2cb520f9f53c7cc531e Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 19:48:12 +0800 Subject: [PATCH 02/14] [DLMED] add RGB color GIF Signed-off-by: Nic Ma --- events.out.tfevents.1635940012.apt-sh-ai | Bin 0 -> 9164 bytes monai/visualize/img2tensorboard.py | 3 +++ tests/test_plot_2d_or_3d_image.py | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 events.out.tfevents.1635940012.apt-sh-ai diff --git a/events.out.tfevents.1635940012.apt-sh-ai b/events.out.tfevents.1635940012.apt-sh-ai new file mode 100644 index 0000000000000000000000000000000000000000..98f743524597dd977198311c7bd9e13ab086dc9a GIT binary patch literal 9164 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 Sk$4Vi^XT8=xTnguqJINm$(G{) literal 0 HcmV?d00001 diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 1f45cd33eb..3cfb4628fe 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -235,6 +235,9 @@ def plot_2d_or_3d_image( if d.ndim >= 4: spatial = d.shape[-3:] d = d.reshape([-1] + list(spatial)) + if d.shape[0] == 3 and max_channels == 3 and has_tensorboardX and isinstance(writer, SummaryWriterX): # RGB + writer.add_video(tag, d[None], step, fps=max_frames, dataformats="NCHWT") + return for j, d3 in enumerate(d[:max_channels]): d3 = rescale_array(d3, 0, 255) diff --git a/tests/test_plot_2d_or_3d_image.py b/tests/test_plot_2d_or_3d_image.py index 8d980a2dd1..bd0ff9faa8 100644 --- a/tests/test_plot_2d_or_3d_image.py +++ b/tests/test_plot_2d_or_3d_image.py @@ -21,7 +21,7 @@ from monai.utils import optional_import from tests.utils import SkipIfNoModule -SummaryWriterX, has_tensorboardX = optional_import("tensorboardX", name="SummaryWriter") +SummaryWriterX, _ = optional_import("tensorboardX", name="SummaryWriter") TEST_CASE_1 = [(1, 1, 10, 10)] @@ -36,7 +36,7 @@ class TestPlot2dOr3dImage(unittest.TestCase): @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5]) - def test_tb_image_shape(self, shape): + def test_tb_image(self, shape): with tempfile.TemporaryDirectory() as tempdir: writer = SummaryWriter(log_dir=tempdir) plot_2d_or_3d_image(torch.zeros(shape), 0, writer) @@ -46,7 +46,7 @@ def test_tb_image_shape(self, shape): @SkipIfNoModule("tensorboardX") @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5]) - def test_tb_image_shape(self, shape): + def test_tbx_image(self, shape): with tempfile.TemporaryDirectory() as tempdir: writer = SummaryWriterX(log_dir=tempdir) plot_2d_or_3d_image(torch.zeros(shape), 0, writer) @@ -54,6 +54,16 @@ def test_tb_image_shape(self, shape): writer.close() self.assertTrue(len(glob.glob(tempdir)) > 0) + @SkipIfNoModule("tensorboardX") + @parameterized.expand([TEST_CASE_5]) + def test_tbx_video(self, shape): + tempdir = "./" + writer = SummaryWriterX(log_dir=tempdir) + plot_2d_or_3d_image(torch.rand(shape), 0, writer, max_channels=3) + writer.flush() + writer.close() + self.assertTrue(len(glob.glob(tempdir)) > 0) + if __name__ == "__main__": unittest.main() From b0228fa363d64c8f2c602db78f780b935d22a66d Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 20:10:44 +0800 Subject: [PATCH 03/14] [DLMED] add support to handlers Signed-off-by: Nic Ma --- monai/handlers/tensorboard_handlers.py | 34 ++++++++++++++------------ monai/visualize/img2tensorboard.py | 6 ++--- tests/test_plot_2d_or_3d_image.py | 12 ++++----- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/monai/handlers/tensorboard_handlers.py b/monai/handlers/tensorboard_handlers.py index d294d0adb5..cc893ac8bf 100644 --- a/monai/handlers/tensorboard_handlers.py +++ b/monai/handlers/tensorboard_handlers.py @@ -10,7 +10,7 @@ # limitations under the License. import warnings -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence +from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Union import numpy as np import torch @@ -23,9 +23,11 @@ if TYPE_CHECKING: from ignite.engine import Engine from torch.utils.tensorboard import SummaryWriter + from tensorboardX import SummaryWriter as SummaryWriterX else: Engine, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Engine") SummaryWriter, _ = optional_import("torch.utils.tensorboard", name="SummaryWriter") + SummaryWriterX, _ = optional_import("tensorboardX", name="SummaryWriter") DEFAULT_TAG = "Loss" @@ -35,13 +37,13 @@ class TensorBoardHandler: Base class for the handlers to write data into TensorBoard. Args: - summary_writer: user can specify TensorBoard SummaryWriter, - default to create a new writer. + summary_writer: user can specify TensorBoard or TensorBoardX SummaryWriter, + default to create a new TensorBoard writer. log_dir: if using default SummaryWriter, write logs to this directory, default is `./runs`. """ - def __init__(self, summary_writer: Optional[SummaryWriter] = None, log_dir: str = "./runs"): + def __init__(self, summary_writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, log_dir: str = "./runs"): if summary_writer is None: self._writer = SummaryWriter(log_dir=log_dir) self.internal_writer = True @@ -81,11 +83,11 @@ class TensorBoardStatsHandler(TensorBoardHandler): def __init__( self, - summary_writer: Optional[SummaryWriter] = None, + summary_writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, log_dir: str = "./runs", - epoch_event_writer: Optional[Callable[[Engine, SummaryWriter], Any]] = None, + epoch_event_writer: Optional[Callable[[Engine, Union[SummaryWriter, SummaryWriterX]], Any]] = None, epoch_interval: int = 1, - iteration_event_writer: Optional[Callable[[Engine, SummaryWriter], Any]] = None, + iteration_event_writer: Optional[Callable[[Engine, Union[SummaryWriter, SummaryWriterX]], Any]] = None, iteration_interval: int = 1, output_transform: Callable = lambda x: x[0], global_epoch_transform: Callable = lambda x: x, @@ -94,8 +96,8 @@ def __init__( ) -> None: """ Args: - summary_writer: user can specify TensorBoard SummaryWriter, - default to create a new writer. + summary_writer: user can specify TensorBoard or TensorBoardX SummaryWriter, + default to create a new TensorBoard writer. log_dir: if using default SummaryWriter, write logs to this directory, default is `./runs`. epoch_event_writer: customized callable TensorBoard writer for epoch level. Must accept parameter "engine" and "summary_writer", use default event writer if None. @@ -172,7 +174,7 @@ def iteration_completed(self, engine: Engine) -> None: else: self._default_iteration_writer(engine, self._writer) - def _default_epoch_writer(self, engine: Engine, writer: SummaryWriter) -> None: + def _default_epoch_writer(self, engine: Engine, writer: Union[SummaryWriter, SummaryWriterX]) -> None: """ Execute epoch level event write operation. Default to write the values from Ignite `engine.state.metrics` dict and @@ -180,7 +182,7 @@ def _default_epoch_writer(self, engine: Engine, writer: SummaryWriter) -> None: Args: engine: Ignite Engine, it can be a trainer, validator or evaluator. - writer: TensorBoard writer, created in TensorBoardHandler. + writer: TensorBoard or TensorBoardX writer, passed or created in TensorBoardHandler. """ current_epoch = self.global_epoch_transform(engine.state.epoch) @@ -194,7 +196,7 @@ def _default_epoch_writer(self, engine: Engine, writer: SummaryWriter) -> None: writer.add_scalar(attr, getattr(engine.state, attr, None), current_epoch) writer.flush() - def _default_iteration_writer(self, engine: Engine, writer: SummaryWriter) -> None: + def _default_iteration_writer(self, engine: Engine, writer: Union[SummaryWriter, SummaryWriterX]) -> None: """ Execute iteration level event write operation based on Ignite `engine.state.output` data. Extract the values from `self.output_transform(engine.state.output)`. @@ -203,7 +205,7 @@ def _default_iteration_writer(self, engine: Engine, writer: SummaryWriter) -> No Args: engine: Ignite Engine, it can be a trainer, validator or evaluator. - writer: TensorBoard writer, created in TensorBoardHandler. + writer: TensorBoard or TensorBoardX writer, passed or created in TensorBoardHandler. """ loss = self.output_transform(engine.state.output) @@ -264,7 +266,7 @@ class TensorBoardImageHandler(TensorBoardHandler): def __init__( self, - summary_writer: Optional[SummaryWriter] = None, + summary_writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, log_dir: str = "./runs", interval: int = 1, epoch_level: bool = True, @@ -277,8 +279,8 @@ def __init__( ) -> None: """ Args: - summary_writer: user can specify TensorBoard SummaryWriter, - default to create a new writer. + summary_writer: user can specify TensorBoard or TensorBoardX SummaryWriter, + default to create a new TensorBoard writer. log_dir: if using default SummaryWriter, write logs to this directory, default is `./runs`. interval: plot content from engine.state every N epochs or every N iterations, default is 1. epoch_level: plot content from engine.state every N epochs or N iterations. `True` is epoch level, diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 3cfb4628fe..80ba20e344 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -33,9 +33,6 @@ SummaryX, has_tensorboardX = optional_import("tensorboardX.proto.summary_pb2", name="Summary") SummaryWriterX, has_tensorboardX = optional_import("tensorboardX", name="SummaryWriter") - - - __all__ = ["make_animated_gif_summary", "add_animated_gif", "add_animated_gif_no_channels", "plot_2d_or_3d_image"] @@ -200,13 +197,14 @@ def plot_2d_or_3d_image( Note: Plot 3D or 2D image(with more than 3 channels) as separate images. + And if writer is from TensorBoardX, data has 3 channels and `max_channels=3`, will plot as RGB GIF image. Args: data: target data to be plotted as image on the TensorBoard. The data is expected to have 'NCHW[D]' dimensions or a list of data with `CHW[D]` dimensions, and only plot the first in the batch. step: current step to plot in a chart. - writer: specify TensorBoard SummaryWriter to plot the image. + writer: specify TensorBoard or TensorBoardX SummaryWriter to plot the image. index: plot which element in the input data batch, default is the first element. max_channels: number of channels to plot. max_frames: number of frames for 2D-t plot. diff --git a/tests/test_plot_2d_or_3d_image.py b/tests/test_plot_2d_or_3d_image.py index bd0ff9faa8..7ca6de36ca 100644 --- a/tests/test_plot_2d_or_3d_image.py +++ b/tests/test_plot_2d_or_3d_image.py @@ -57,12 +57,12 @@ def test_tbx_image(self, shape): @SkipIfNoModule("tensorboardX") @parameterized.expand([TEST_CASE_5]) def test_tbx_video(self, shape): - tempdir = "./" - writer = SummaryWriterX(log_dir=tempdir) - plot_2d_or_3d_image(torch.rand(shape), 0, writer, max_channels=3) - writer.flush() - writer.close() - self.assertTrue(len(glob.glob(tempdir)) > 0) + with tempfile.TemporaryDirectory() as tempdir: + writer = SummaryWriterX(log_dir=tempdir) + plot_2d_or_3d_image(torch.rand(shape), 0, writer, max_channels=3) + writer.flush() + writer.close() + self.assertTrue(len(glob.glob(tempdir)) > 0) if __name__ == "__main__": From dd86c7c85934cb75c10e289c80f5396fcd6cf30c Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 20:17:53 +0800 Subject: [PATCH 04/14] [DLMED] add optional import Signed-off-by: Nic Ma --- docs/requirements.txt | 1 + docs/source/installation.md | 4 ++-- monai/config/deviceconfig.py | 2 ++ requirements-dev.txt | 2 ++ setup.cfg | 3 +++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index cefb47e7e0..55bb8f0cb0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -22,3 +22,4 @@ pandas einops transformers mlflow +tensorboardX diff --git a/docs/source/installation.md b/docs/source/installation.md index 2649756815..008ff00e79 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -174,9 +174,9 @@ Since MONAI v0.2.0, the extras syntax such as `pip install 'monai[nibabel]'` is - The options are ``` -[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib] +[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, cucim, openslide, pandas, einops, transformers, mlflow, matplotlib, tensorboardX] ``` which correspond to `nibabel`, `scikit-image`, `pillow`, `tensorboard`, -`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, respectively. +`gdown`, `pytorch-ignite`, `torchvision`, `itk`, `tqdm`, `lmdb`, `psutil`, `cucim`, `openslide-python`, `pandas`, `einops`, `transformers`, `mlflow`, `matplotlib`, `tensorboardX` respectively. - `pip install 'monai[all]'` installs all the optional dependencies. diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index e542da14ab..782faea14e 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -75,6 +75,8 @@ def get_optional_config_values(): output["einops"] = get_package_version("einops") output["transformers"] = get_package_version("transformers") output["mlflow"] = get_package_version("mlflow") + output["matplotlib"] = get_package_version("matplotlib") + output["tensorboardX"] = get_package_version("tensorboardX") return output diff --git a/requirements-dev.txt b/requirements-dev.txt index 56d5709cb3..ed8b61e92b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -41,3 +41,5 @@ einops transformers mlflow matplotlib +tensorboardX +moviepy diff --git a/setup.cfg b/setup.cfg index 6f94bee7c0..1a87d0d91a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ all = transformers mlflow matplotlib + tensorboardX nibabel = nibabel skimage = @@ -83,6 +84,8 @@ mlflow = mlflow matplotlib = matplotlib +tensorboardX = + tensorboardX [flake8] select = B,C,E,F,N,P,T4,W,B9 From 75abfcca0f30279d94e355e4273313602dd5fac1 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 20:20:14 +0800 Subject: [PATCH 05/14] [DLMED] format code Signed-off-by: Nic Ma --- monai/handlers/tensorboard_handlers.py | 2 +- monai/visualize/img2tensorboard.py | 5 +++-- tests/test_plot_2d_or_3d_image.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/monai/handlers/tensorboard_handlers.py b/monai/handlers/tensorboard_handlers.py index cc893ac8bf..6d7e7222ac 100644 --- a/monai/handlers/tensorboard_handlers.py +++ b/monai/handlers/tensorboard_handlers.py @@ -22,8 +22,8 @@ Events, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Events") if TYPE_CHECKING: from ignite.engine import Engine - from torch.utils.tensorboard import SummaryWriter from tensorboardX import SummaryWriter as SummaryWriterX + from torch.utils.tensorboard import SummaryWriter else: Engine, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Engine") SummaryWriter, _ = optional_import("torch.utils.tensorboard", name="SummaryWriter") diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 80ba20e344..8d1606b6d0 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -23,9 +23,10 @@ if TYPE_CHECKING: from tensorboard.compat.proto.summary_pb2 import Summary - from torch.utils.tensorboard import SummaryWriter - from tensorboardX.proto.summary_pb2 import Summary as SummaryX from tensorboardX import SummaryWriter as SummaryWriterX + from tensorboardX.proto.summary_pb2 import Summary as SummaryX + from torch.utils.tensorboard import SummaryWriter + has_tensorboardX = True else: Summary, _ = optional_import("tensorboard.compat.proto.summary_pb2", name="Summary") diff --git a/tests/test_plot_2d_or_3d_image.py b/tests/test_plot_2d_or_3d_image.py index 7ca6de36ca..77acb178c5 100644 --- a/tests/test_plot_2d_or_3d_image.py +++ b/tests/test_plot_2d_or_3d_image.py @@ -17,8 +17,8 @@ from parameterized import parameterized from torch.utils.tensorboard import SummaryWriter -from monai.visualize import plot_2d_or_3d_image from monai.utils import optional_import +from monai.visualize import plot_2d_or_3d_image from tests.utils import SkipIfNoModule SummaryWriterX, _ = optional_import("tensorboardX", name="SummaryWriter") From 43f180d028b420bb069d303c87b5a5f32e1306b8 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 20:31:43 +0800 Subject: [PATCH 06/14] =?UTF-8?q?[DLMED]=20fix=20flake8=E2=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nic Ma --- monai/visualize/img2tensorboard.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 8d1606b6d0..fa056a2f71 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -27,12 +27,12 @@ from tensorboardX.proto.summary_pb2 import Summary as SummaryX from torch.utils.tensorboard import SummaryWriter - has_tensorboardX = True + has_tensorboardx = True else: Summary, _ = optional_import("tensorboard.compat.proto.summary_pb2", name="Summary") SummaryWriter, _ = optional_import("torch.utils.tensorboard", name="SummaryWriter") - SummaryX, has_tensorboardX = optional_import("tensorboardX.proto.summary_pb2", name="Summary") - SummaryWriterX, has_tensorboardX = optional_import("tensorboardX", name="SummaryWriter") + SummaryX, has_tensorboardx = optional_import("tensorboardX.proto.summary_pb2", name="Summary") + SummaryWriterX, has_tensorboardx = optional_import("tensorboardX", name="SummaryWriter") __all__ = ["make_animated_gif_summary", "add_animated_gif", "add_animated_gif_no_channels", "plot_2d_or_3d_image"] @@ -65,7 +65,7 @@ def _image3_animated_gif( img_str += b_data img_str += b"\x3B" - summary = SummaryX if has_tensorboardX and isinstance(writer, SummaryWriterX) else Summary + summary = SummaryX if has_tensorboardx and isinstance(writer, SummaryWriterX) else Summary summary_image_str = summary.Image(height=10, width=10, colorspace=1, encoded_image_string=img_str) image_summary = summary.Value(tag=tag, image=summary_image_str) return summary(value=[image_summary]) @@ -234,7 +234,7 @@ def plot_2d_or_3d_image( if d.ndim >= 4: spatial = d.shape[-3:] d = d.reshape([-1] + list(spatial)) - if d.shape[0] == 3 and max_channels == 3 and has_tensorboardX and isinstance(writer, SummaryWriterX): # RGB + if d.shape[0] == 3 and max_channels == 3 and has_tensorboardx and isinstance(writer, SummaryWriterX): # RGB writer.add_video(tag, d[None], step, fps=max_frames, dataformats="NCHWT") return From ffc11a4bfb28f93b27c5dbbee1aa92c1331e6b93 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 21:15:17 +0800 Subject: [PATCH 07/14] [DLMED] fix typo Signed-off-by: Nic Ma --- events.out.tfevents.1635940012.apt-sh-ai | Bin 9164 -> 0 bytes monai/visualize/img2tensorboard.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 events.out.tfevents.1635940012.apt-sh-ai diff --git a/events.out.tfevents.1635940012.apt-sh-ai b/events.out.tfevents.1635940012.apt-sh-ai deleted file mode 100644 index 98f743524597dd977198311c7bd9e13ab086dc9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9164 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 Sk$4Vi^XT8=xTnguqJINm$(G{) diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index fa056a2f71..c3d1357cc8 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -40,7 +40,7 @@ def _image3_animated_gif( tag: str, image: Union[np.ndarray, torch.Tensor], - writer: Union[SummaryWriter, SummaryWriterX], + writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, scale_factor: float = 1.0, ): """Function to actually create the animated gif. @@ -74,7 +74,7 @@ def _image3_animated_gif( def make_animated_gif_summary( tag: str, image: Union[np.ndarray, torch.Tensor], - writer: Union[SummaryWriter, SummaryWriterX], + writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, max_out: int = 3, animation_axes: Sequence[int] = (3,), image_axes: Sequence[int] = (1, 2), From e11700560e3ff4e63722361611bea40e43c7efbc Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 22:46:47 +0800 Subject: [PATCH 08/14] [DLMED] fix typing Signed-off-by: Nic Ma --- monai/handlers/tensorboard_handlers.py | 19 +++++++++---------- monai/visualize/img2tensorboard.py | 19 ++++++------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/monai/handlers/tensorboard_handlers.py b/monai/handlers/tensorboard_handlers.py index 6d7e7222ac..be48bc9e0b 100644 --- a/monai/handlers/tensorboard_handlers.py +++ b/monai/handlers/tensorboard_handlers.py @@ -10,7 +10,7 @@ # limitations under the License. import warnings -from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence import numpy as np import torch @@ -20,14 +20,13 @@ from monai.visualize import plot_2d_or_3d_image Events, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Events") + if TYPE_CHECKING: from ignite.engine import Engine - from tensorboardX import SummaryWriter as SummaryWriterX from torch.utils.tensorboard import SummaryWriter else: Engine, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Engine") SummaryWriter, _ = optional_import("torch.utils.tensorboard", name="SummaryWriter") - SummaryWriterX, _ = optional_import("tensorboardX", name="SummaryWriter") DEFAULT_TAG = "Loss" @@ -43,7 +42,7 @@ class TensorBoardHandler: """ - def __init__(self, summary_writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, log_dir: str = "./runs"): + def __init__(self, summary_writer: Optional[SummaryWriter] = None, log_dir: str = "./runs"): if summary_writer is None: self._writer = SummaryWriter(log_dir=log_dir) self.internal_writer = True @@ -83,11 +82,11 @@ class TensorBoardStatsHandler(TensorBoardHandler): def __init__( self, - summary_writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, + summary_writer: Optional[SummaryWriter] = None, log_dir: str = "./runs", - epoch_event_writer: Optional[Callable[[Engine, Union[SummaryWriter, SummaryWriterX]], Any]] = None, + epoch_event_writer: Optional[Callable[[Engine, SummaryWriter], Any]] = None, epoch_interval: int = 1, - iteration_event_writer: Optional[Callable[[Engine, Union[SummaryWriter, SummaryWriterX]], Any]] = None, + iteration_event_writer: Optional[Callable[[Engine, SummaryWriter], Any]] = None, iteration_interval: int = 1, output_transform: Callable = lambda x: x[0], global_epoch_transform: Callable = lambda x: x, @@ -174,7 +173,7 @@ def iteration_completed(self, engine: Engine) -> None: else: self._default_iteration_writer(engine, self._writer) - def _default_epoch_writer(self, engine: Engine, writer: Union[SummaryWriter, SummaryWriterX]) -> None: + def _default_epoch_writer(self, engine: Engine, writer: SummaryWriter) -> None: """ Execute epoch level event write operation. Default to write the values from Ignite `engine.state.metrics` dict and @@ -196,7 +195,7 @@ def _default_epoch_writer(self, engine: Engine, writer: Union[SummaryWriter, Sum writer.add_scalar(attr, getattr(engine.state, attr, None), current_epoch) writer.flush() - def _default_iteration_writer(self, engine: Engine, writer: Union[SummaryWriter, SummaryWriterX]) -> None: + def _default_iteration_writer(self, engine: Engine, writer: SummaryWriter) -> None: """ Execute iteration level event write operation based on Ignite `engine.state.output` data. Extract the values from `self.output_transform(engine.state.output)`. @@ -266,7 +265,7 @@ class TensorBoardImageHandler(TensorBoardHandler): def __init__( self, - summary_writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, + summary_writer: Optional[SummaryWriter] = None, log_dir: str = "./runs", interval: int = 1, epoch_level: bool = True, diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index c3d1357cc8..aa46cda09c 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -20,28 +20,21 @@ PIL, _ = optional_import("PIL") GifImage, _ = optional_import("PIL.GifImagePlugin", name="Image") +SummaryX, _ = optional_import("tensorboardX.proto.summary_pb2", name="Summary") +SummaryWriterX, has_tensorboardx = optional_import("tensorboardX", name="SummaryWriter") if TYPE_CHECKING: from tensorboard.compat.proto.summary_pb2 import Summary - from tensorboardX import SummaryWriter as SummaryWriterX - from tensorboardX.proto.summary_pb2 import Summary as SummaryX from torch.utils.tensorboard import SummaryWriter - - has_tensorboardx = True else: Summary, _ = optional_import("tensorboard.compat.proto.summary_pb2", name="Summary") SummaryWriter, _ = optional_import("torch.utils.tensorboard", name="SummaryWriter") - SummaryX, has_tensorboardx = optional_import("tensorboardX.proto.summary_pb2", name="Summary") - SummaryWriterX, has_tensorboardx = optional_import("tensorboardX", name="SummaryWriter") __all__ = ["make_animated_gif_summary", "add_animated_gif", "add_animated_gif_no_channels", "plot_2d_or_3d_image"] def _image3_animated_gif( - tag: str, - image: Union[np.ndarray, torch.Tensor], - writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, - scale_factor: float = 1.0, + tag: str, image: Union[np.ndarray, torch.Tensor], writer: Optional[SummaryWriter] = None, scale_factor: float = 1.0 ): """Function to actually create the animated gif. @@ -74,7 +67,7 @@ def _image3_animated_gif( def make_animated_gif_summary( tag: str, image: Union[np.ndarray, torch.Tensor], - writer: Optional[Union[SummaryWriter, SummaryWriterX]] = None, + writer: Optional[SummaryWriter] = None, max_out: int = 3, animation_axes: Sequence[int] = (3,), image_axes: Sequence[int] = (1, 2), @@ -118,7 +111,7 @@ def make_animated_gif_summary( def add_animated_gif( - writer: Union[SummaryWriter, SummaryWriterX], + writer: SummaryWriter, tag: str, image_tensor: Union[np.ndarray, torch.Tensor], max_out: int, @@ -188,7 +181,7 @@ def add_animated_gif_no_channels( def plot_2d_or_3d_image( data: Union[NdarrayTensor, List[NdarrayTensor]], step: int, - writer: Union[SummaryWriter, SummaryWriterX], + writer: SummaryWriter, index: int = 0, max_channels: int = 1, max_frames: int = 64, From 87074f6367fbd6b8cc6440118fdc283faf0ac724 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 3 Nov 2021 23:19:22 +0800 Subject: [PATCH 09/14] [DLEMD] test python 3.6 Signed-off-by: Nic Ma --- monai/config/deviceconfig.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 782faea14e..e542da14ab 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -75,8 +75,6 @@ def get_optional_config_values(): output["einops"] = get_package_version("einops") output["transformers"] = get_package_version("transformers") output["mlflow"] = get_package_version("mlflow") - output["matplotlib"] = get_package_version("matplotlib") - output["tensorboardX"] = get_package_version("tensorboardX") return output From a14ee6131c4ee9772efa14e94b484f9f779b0635 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 4 Nov 2021 13:48:35 +0000 Subject: [PATCH 10/14] test remove typing Signed-off-by: Wenqi Li --- monai/visualize/img2tensorboard.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index aa46cda09c..439f84cdbd 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -33,9 +33,7 @@ __all__ = ["make_animated_gif_summary", "add_animated_gif", "add_animated_gif_no_channels", "plot_2d_or_3d_image"] -def _image3_animated_gif( - tag: str, image: Union[np.ndarray, torch.Tensor], writer: Optional[SummaryWriter] = None, scale_factor: float = 1.0 -): +def _image3_animated_gif(tag: str, image: Union[np.ndarray, torch.Tensor], writer, scale_factor: float = 1.0): """Function to actually create the animated gif. Args: @@ -67,7 +65,7 @@ def _image3_animated_gif( def make_animated_gif_summary( tag: str, image: Union[np.ndarray, torch.Tensor], - writer: Optional[SummaryWriter] = None, + writer=None, max_out: int = 3, animation_axes: Sequence[int] = (3,), image_axes: Sequence[int] = (1, 2), From 4aadb5db6bf3ac1333c20402ce18202f1a7cec43 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 5 Nov 2021 20:15:37 +0800 Subject: [PATCH 11/14] [DLMED] update according to comments Signed-off-by: Nic Ma --- monai/handlers/tensorboard_handlers.py | 3 +- monai/visualize/img2tensorboard.py | 56 +++++++++++++------------- tests/test_plot_2d_or_3d_image.py | 4 +- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/monai/handlers/tensorboard_handlers.py b/monai/handlers/tensorboard_handlers.py index be48bc9e0b..490ba5d2d1 100644 --- a/monai/handlers/tensorboard_handlers.py +++ b/monai/handlers/tensorboard_handlers.py @@ -244,6 +244,7 @@ class TensorBoardImageHandler(TensorBoardHandler): 2D output (shape in Batch, channel, H, W) will be shown as simple image using the first element in the batch, for 3D to ND output (shape in Batch, channel, H, W, D) input, each of ``self.max_channels`` number of images' last three dimensions will be shown as animated GIF along the last axis (typically Depth). + And if writer is from TensorBoardX, data has 3 channels and `max_channels=3`, will plot as RGB video. It can be used for any Ignite Engine (trainer, validator and evaluator). User can easily add it to engine for any expected Event, for example: ``EPOCH_COMPLETED``, @@ -300,7 +301,7 @@ def __init__( For example, in evaluation, the evaluator engine needs to know current epoch from trainer. index: plot which element in a data batch, default is the first element. max_channels: number of channels to plot. - max_frames: number of frames for 2D-t plot. + max_frames: if plot 3D RGB image as video in TensorBoardX, set the FPS to `max_frames`. """ super().__init__(summary_writer=summary_writer, log_dir=log_dir) self.interval = interval diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 439f84cdbd..eef1b2e764 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -78,7 +78,7 @@ def make_animated_gif_summary( tag: Data identifier image: The image, expected to be in CHWD format writer: the tensorboard writer to plot image - max_out: maximum number of slices to animate through + max_out: maximum number of image channels to animate through animation_axes: axis to animate on (not currently used) image_axes: axes of image (not currently used) other_indices: (not currently used) @@ -100,11 +100,12 @@ def make_animated_gif_summary( slicing.append(slice(other_ind, other_ind + 1)) image = image[tuple(slicing)] + summary_op = [] for it_i in range(min(max_out, list(image.shape)[0])): one_channel_img: Union[torch.Tensor, np.ndarray] = ( image[it_i, :, :, :].squeeze(dim=0) if isinstance(image, torch.Tensor) else image[it_i, :, :, :] ) - summary_op = _image3_animated_gif(tag + suffix.format(it_i), one_channel_img, writer, scale_factor) + summary_op.append(_image3_animated_gif(tag + suffix.format(it_i), one_channel_img, writer, scale_factor)) return summary_op @@ -112,8 +113,8 @@ def add_animated_gif( writer: SummaryWriter, tag: str, image_tensor: Union[np.ndarray, torch.Tensor], - max_out: int, - scale_factor: float, + max_out: int = 3, + scale_factor: float = 1.0, global_step: Optional[int] = None, ) -> None: """Creates an animated gif out of an image tensor in 'CHWD' format and writes it with SummaryWriter. @@ -122,31 +123,31 @@ def add_animated_gif( writer: Tensorboard SummaryWriter to write to tag: Data identifier image_tensor: tensor for the image to add, expected to be in CHWD format - max_out: maximum number of slices to animate through + max_out: maximum number of image channels to animate through scale_factor: amount to multiply values by. If the image data is between 0 and 1, using 255 for this value will scale it to displayable range global_step: Global step value to record """ - writer._get_file_writer().add_summary( - make_animated_gif_summary( - tag=tag, - image=image_tensor, - writer=writer, - max_out=max_out, - animation_axes=[1], - image_axes=[2, 3], - scale_factor=scale_factor, - ), - global_step, + summary = make_animated_gif_summary( + tag=tag, + image=image_tensor, + writer=writer, + max_out=max_out, + animation_axes=[1], + image_axes=[2, 3], + scale_factor=scale_factor, ) + for s in summary: + # add GIF for every channel separately + writer._get_file_writer().add_summary(s, global_step) def add_animated_gif_no_channels( writer: SummaryWriter, tag: str, image_tensor: Union[np.ndarray, torch.Tensor], - max_out: int, - scale_factor: float, + max_out: int = 3, + scale_factor: float = 1.0, global_step: Optional[int] = None, ) -> None: """Creates an animated gif out of an image tensor in 'HWD' format that does not have @@ -157,7 +158,7 @@ def add_animated_gif_no_channels( writer: Tensorboard SummaryWriter to write to tag: Data identifier image_tensor: tensor for the image to add, expected to be in HWD format - max_out: maximum number of slices to animate through + max_out: maximum number of image channels to animate through scale_factor: amount to multiply values by. If the image data is between 0 and 1, using 255 for this value will scale it to displayable range global_step: Global step value to record @@ -171,7 +172,7 @@ def add_animated_gif_no_channels( animation_axes=[1], image_axes=[1, 2], scale_factor=scale_factor, - ), + )[0], global_step, ) @@ -182,14 +183,14 @@ def plot_2d_or_3d_image( writer: SummaryWriter, index: int = 0, max_channels: int = 1, - max_frames: int = 64, + max_frames: int = 24, tag: str = "output", ) -> None: """Plot 2D or 3D image on the TensorBoard, 3D image will be converted to GIF image. Note: Plot 3D or 2D image(with more than 3 channels) as separate images. - And if writer is from TensorBoardX, data has 3 channels and `max_channels=3`, will plot as RGB GIF image. + And if writer is from TensorBoardX, data has 3 channels and `max_channels=3`, will plot as RGB video. Args: data: target data to be plotted as image on the TensorBoard. @@ -199,7 +200,7 @@ def plot_2d_or_3d_image( writer: specify TensorBoard or TensorBoardX SummaryWriter to plot the image. index: plot which element in the input data batch, default is the first element. max_channels: number of channels to plot. - max_frames: number of frames for 2D-t plot. + max_frames: if plot 3D RGB image as video in TensorBoardX, set the FPS to `max_frames`. tag: tag of the plotted image on TensorBoard. """ data_index = data[index] @@ -228,8 +229,9 @@ def plot_2d_or_3d_image( if d.shape[0] == 3 and max_channels == 3 and has_tensorboardx and isinstance(writer, SummaryWriterX): # RGB writer.add_video(tag, d[None], step, fps=max_frames, dataformats="NCHWT") return - - for j, d3 in enumerate(d[:max_channels]): - d3 = rescale_array(d3, 0, 255) - add_animated_gif(writer, f"{tag}_HWD_{j}", d3[None], max_frames, 1.0, step) + # scale data to 0 - 255 for visualization + max_channels = min(max_channels, d.shape[0]) + d = np.stack([rescale_array(i, 0, 255) for i in d[:max_channels]], axis=0) + # will plot every channel as a separate GIF image + add_animated_gif(writer, f"{tag}_HWD", d, max_out=max_channels, global_step=step) return diff --git a/tests/test_plot_2d_or_3d_image.py b/tests/test_plot_2d_or_3d_image.py index 77acb178c5..c645c8ff86 100644 --- a/tests/test_plot_2d_or_3d_image.py +++ b/tests/test_plot_2d_or_3d_image.py @@ -39,7 +39,7 @@ class TestPlot2dOr3dImage(unittest.TestCase): def test_tb_image(self, shape): with tempfile.TemporaryDirectory() as tempdir: writer = SummaryWriter(log_dir=tempdir) - plot_2d_or_3d_image(torch.zeros(shape), 0, writer) + plot_2d_or_3d_image(torch.zeros(shape), 0, writer, max_channels=20) writer.flush() writer.close() self.assertTrue(len(glob.glob(tempdir)) > 0) @@ -49,7 +49,7 @@ def test_tb_image(self, shape): def test_tbx_image(self, shape): with tempfile.TemporaryDirectory() as tempdir: writer = SummaryWriterX(log_dir=tempdir) - plot_2d_or_3d_image(torch.zeros(shape), 0, writer) + plot_2d_or_3d_image(torch.zeros(shape), 0, writer, max_channels=2) writer.flush() writer.close() self.assertTrue(len(glob.glob(tempdir)) > 0) From 94ad219e40a227e3055fe1619fd027f00aa4d421 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 5 Nov 2021 22:39:52 +0800 Subject: [PATCH 12/14] [DLMED] fix tests Signed-off-by: Nic Ma --- tests/test_img2tensorboard.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_img2tensorboard.py b/tests/test_img2tensorboard.py index bd0369868e..bf6890bcad 100644 --- a/tests/test_img2tensorboard.py +++ b/tests/test_img2tensorboard.py @@ -29,9 +29,10 @@ def test_write_gray(self): image_axes=(1, 2), scale_factor=253.0, ) - assert isinstance( - summary_object_np, tensorboard.compat.proto.summary_pb2.Summary - ), "make_animated_gif_summary must return a tensorboard.summary object from numpy array" + for s in summary_object_np: + assert isinstance( + s, tensorboard.compat.proto.summary_pb2.Summary + ), "make_animated_gif_summary must return a tensorboard.summary object from numpy array" tensorarr = torch.tensor(nparr) summary_object_tensor = make_animated_gif_summary( @@ -42,9 +43,10 @@ def test_write_gray(self): image_axes=(1, 2), scale_factor=253.0, ) - assert isinstance( - summary_object_tensor, tensorboard.compat.proto.summary_pb2.Summary - ), "make_animated_gif_summary must return a tensorboard.summary object from tensor input" + for s in summary_object_tensor: + assert isinstance( + s, tensorboard.compat.proto.summary_pb2.Summary + ), "make_animated_gif_summary must return a tensorboard.summary object from tensor input" if __name__ == "__main__": From af3808ccad47046b19fdb2b11ce23b7208c03102 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Sun, 7 Nov 2021 08:34:38 +0000 Subject: [PATCH 13/14] temp fix Signed-off-by: Wenqi Li --- runtests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtests.sh b/runtests.sh index a77f9decd5..c0c683d051 100755 --- a/runtests.sh +++ b/runtests.sh @@ -570,5 +570,5 @@ if [ $doCoverage = true ] then echo "${separator}${blue}coverage${noColor}" ${cmdPrefix}${PY_EXE} -m coverage combine --append .coverage/ - ${cmdPrefix}${PY_EXE} -m coverage report + ${cmdPrefix}${PY_EXE} -m coverage report -i fi From 52a6cd8d45bdd5c93391ba793f20a88b7ba07d2e Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 8 Nov 2021 19:27:50 +0800 Subject: [PATCH 14/14] [DLMED] remove moviepy Signed-off-by: Nic Ma --- requirements-dev.txt | 1 - runtests.sh | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ed8b61e92b..1d9d52bca5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -42,4 +42,3 @@ transformers mlflow matplotlib tensorboardX -moviepy diff --git a/runtests.sh b/runtests.sh index c0c683d051..a77f9decd5 100755 --- a/runtests.sh +++ b/runtests.sh @@ -570,5 +570,5 @@ if [ $doCoverage = true ] then echo "${separator}${blue}coverage${noColor}" ${cmdPrefix}${PY_EXE} -m coverage combine --append .coverage/ - ${cmdPrefix}${PY_EXE} -m coverage report -i + ${cmdPrefix}${PY_EXE} -m coverage report fi