diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e805c1d..60e576ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Fixed - `Data.squeeze`: axes of output object now inherit units from axes of the input object +### Changed +- use `pillow` directly and remove `ImageIO` dependency + ## [3.6.3] ### Changed @@ -74,6 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - new artist helper function: `norm_from_channel` - new artist helper function: `ticks_from_norm` - new artist iterator `ChopHandler` +- `artists.stitch_to_animation`: new kwargs for more gif customization ### Fixed - fixed Quick2D/Quick1D issues where collapsing unused dims did not work diff --git a/WrightTools/artists/_helpers.py b/WrightTools/artists/_helpers.py index 4b05a950..2f328666 100644 --- a/WrightTools/artists/_helpers.py +++ b/WrightTools/artists/_helpers.py @@ -3,7 +3,7 @@ # --- import -------------------------------------------------------------------------------------- -import os +import pathlib import numpy as np @@ -15,6 +15,7 @@ from matplotlib.colors import Normalize, CenteredNorm, TwoSlopeNorm from mpl_toolkits.axes_grid1 import make_axes_locatable +from typing import List import imageio.v3 as iio import warnings @@ -876,7 +877,7 @@ def savefig(path, fig=None, close=True, **kwargs): if fig is None: fig = plt.gcf() - path = os.path.abspath(path) + path = pathlib.Path(path).resolve() kwargs["dpi"] = kwargs.get("dpi", 300) kwargs["transparent"] = kwargs.get("transparent", False) @@ -1084,37 +1085,71 @@ def subplots_adjust(fig=None, inches=1): fig.subplots_adjust(top=top, right=right, bottom=bottom, left=left) -def stitch_to_animation(paths, outpath=None, *, duration=0.5, palettesize=256, verbose=True): - """Stitch a series of images into an animation. - - Currently supports animated gifs, other formats coming as needed. +def stitch_to_animation( + paths, + outpath=None, + duration=0.5, + ignore_alpha: bool = True, + reduce: int = None, + verbose=True, + **kwargs, +): + """Stitch a series of images into a gif. Parameters ---------- paths : list of strings - Filepaths to the images to stitch together, in order of apperence. + Filepaths to the images to stitch together, in order of appearance. outpath : string (optional) - Path of output, including extension. If None, bases output path on path - of first path in `images`. Default is None. + Path of output, including extension. If None, bases output path on `paths[0]`. Default is None. duration : number or list of numbers (optional) Duration of (each) frame in seconds. Default is 0.5. - palettesize : int (optional) - The number of colors in the resulting animation. Input is rounded to - the nearest power of 2. Default is 256. + ignore_alpha : bool (optional) + When True, transparency is excluded from the gif and color palette may be higher res. + reduce : int (optional) + Reduces the resolution along both image dimensions by a factor of `reduce`. verbose : bool (optional) Toggle talkback. Default is True. + + Returns: + -------- + outpath : path-like + path to generated gif """ + import contextlib + from PIL import Image + # parse filename if outpath is None: - outpath = os.path.splitext(paths[0])[0] + ".gif" + outpath = pathlib.Path(paths[0]).with_suffix(".gif") # write t = wt_kit.Timer(verbose=False) - with t, iio.imopen(outpath, "w") as gif: - for p in paths: - frame = iio.imread(p) - gif.write( - frame, plugin="pillow", duration=duration * 1e3, loop=0, palettesize=palettesize - ) + + def process_imgs(imgs: List[Image.Image]): + count = 0 + for img in imgs: + if verbose: + print(f"processing {count+1} / {len(paths)}...") + if ignore_alpha: + img = img.convert("RGB") + if reduce is not None: + img = img.reduce(reduce) + count += 1 + yield img + + with t, contextlib.ExitStack() as stack: + imgs = process_imgs(stack.enter_context(Image.open(p)) for p in paths) + img = next(imgs) + img.save( + fp=outpath, + format="GIF", + append_images=imgs, + save_all=True, + duration=duration * 1e3, + loop=0, + **kwargs, + ) + if verbose: interval = np.round(t.interval, 2) print("gif generated in {0} seconds - saved at {1}".format(interval, outpath)) diff --git a/pyproject.toml b/pyproject.toml index d1852b00..d7a932f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.7" dynamic = ["version"] dependencies = [ "h5py", - "imageio>=2.28.0", + "pillow", "matplotlib>=3.4.0", "numexpr", "numpy>=1.15.0",