Python 枕头透明 gif 无法正常工作
Python Pillow transparent gif isn't working
我正在尝试制作以下图标旋转的 gif:
为了尝试实现这一点,我 运行 使用 Pillow 库 Python 中的以下代码:
from PIL import Image, ImageDraw
images = []
icon = Image.open("cameraIcon.png")
for i in range(0, 360):
images.append(icon.rotate(i))
images[0].save('loadingIcon.gif', format='GIF', save_all=True, append_images=images[1:], duration=40, loop=0, transparency=0)
但是这段代码的输出与我想要的不匹配,我得到了以下 gif :
我不知道发生了什么,希望得到任何帮助,谢谢。
我在使用 PIL 时也遇到了问题,所以这里有一个 ImageMagick 解决方案,让您在我处理它的同时继续前进...
for ((i=0;i<360;i+=5)) ; do
magick camera.png -virtual-pixel transparent -distort SRT "41 41 $i" miff:-
done | magick -background none -dispose background -delay 20 miff:- anim.gif
这里有两个主要问题。
- 您没有指定
Image.save()
的 disposal
参数。此参数控制前一帧相对于背景的布置方式。如果未指定,则前一帧“逗留”到下一帧。添加 disposal=2
应该可以解决该特定问题。
- 其次,截至撰写本文时,pillow 库存在一个 long-standing 问题,会影响透明度。在许多情况下,它会导致透明区域被黑色像素取代。 issue 4640 of the pillow Github repository. There is a workaround that has been provided by one user in related issue 4644 中描述了该问题。只要您不需要支持部分 alpha 透明度,此解决方法就可以正常工作。我发现它会使 alpha 变平并且在那种情况下不起作用。
以下是我使用的代码(改编自问题 4644 中的代码),希望对您有所帮助。您只需使用图像和持续时间调用 save_transparent_gif
函数即可。
# This code adapted from https://github.com/python-pillow/Pillow/issues/4644 to resolve an issue
# described in https://github.com/python-pillow/Pillow/issues/4640
#
# There is a long-standing issue with the Pillow library that messes up GIF transparency by replacing the
# transparent pixels with black pixels (among other issues) when the GIF is saved using PIL.Image.save().
# This code works around the issue and allows us to properly generate transparent GIFs.
from typing import Tuple, List, Union
from collections import defaultdict
from random import randrange
from itertools import chain
from PIL.Image import Image
class TransparentAnimatedGifConverter(object):
_PALETTE_SLOTSET = set(range(256))
def __init__(self, img_rgba: Image, alpha_threshold: int = 0):
self._img_rgba = img_rgba
self._alpha_threshold = alpha_threshold
def _process_pixels(self):
"""Set the transparent pixels to the color 0."""
self._transparent_pixels = set(
idx for idx, alpha in enumerate(
self._img_rgba.getchannel(channel='A').getdata())
if alpha <= self._alpha_threshold)
def _set_parsed_palette(self):
"""Parse the RGB palette color `tuple`s from the palette."""
palette = self._img_p.getpalette()
self._img_p_used_palette_idxs = set(
idx for pal_idx, idx in enumerate(self._img_p_data)
if pal_idx not in self._transparent_pixels)
self._img_p_parsedpalette = dict(
(idx, tuple(palette[idx * 3:idx * 3 + 3]))
for idx in self._img_p_used_palette_idxs)
def _get_similar_color_idx(self):
"""Return a palette index with the closest similar color."""
old_color = self._img_p_parsedpalette[0]
dict_distance = defaultdict(list)
for idx in range(1, 256):
color_item = self._img_p_parsedpalette[idx]
if color_item == old_color:
return idx
distance = sum((
abs(old_color[0] - color_item[0]), # Red
abs(old_color[1] - color_item[1]), # Green
abs(old_color[2] - color_item[2]))) # Blue
dict_distance[distance].append(idx)
return dict_distance[sorted(dict_distance)[0]][0]
def _remap_palette_idx_zero(self):
"""Since the first color is used in the palette, remap it."""
free_slots = self._PALETTE_SLOTSET - self._img_p_used_palette_idxs
new_idx = free_slots.pop() if free_slots else \
self._get_similar_color_idx()
self._img_p_used_palette_idxs.add(new_idx)
self._palette_replaces['idx_from'].append(0)
self._palette_replaces['idx_to'].append(new_idx)
self._img_p_parsedpalette[new_idx] = self._img_p_parsedpalette[0]
del(self._img_p_parsedpalette[0])
def _get_unused_color(self) -> tuple:
""" Return a color for the palette that does not collide with any other already in the palette."""
used_colors = set(self._img_p_parsedpalette.values())
while True:
new_color = (randrange(256), randrange(256), randrange(256))
if new_color not in used_colors:
return new_color
def _process_palette(self):
"""Adjust palette to have the zeroth color set as transparent. Basically, get another palette
index for the zeroth color."""
self._set_parsed_palette()
if 0 in self._img_p_used_palette_idxs:
self._remap_palette_idx_zero()
self._img_p_parsedpalette[0] = self._get_unused_color()
def _adjust_pixels(self):
"""Convert the pixels into their new values."""
if self._palette_replaces['idx_from']:
trans_table = bytearray.maketrans(
bytes(self._palette_replaces['idx_from']),
bytes(self._palette_replaces['idx_to']))
self._img_p_data = self._img_p_data.translate(trans_table)
for idx_pixel in self._transparent_pixels:
self._img_p_data[idx_pixel] = 0
self._img_p.frombytes(data=bytes(self._img_p_data))
def _adjust_palette(self):
"""Modify the palette in the new `Image`."""
unused_color = self._get_unused_color()
final_palette = chain.from_iterable(
self._img_p_parsedpalette.get(x, unused_color) for x in range(256))
self._img_p.putpalette(data=final_palette)
def process(self) -> Image:
"""Return the processed mode `P` `Image`."""
self._img_p = self._img_rgba.convert(mode='P')
self._img_p_data = bytearray(self._img_p.tobytes())
self._palette_replaces = dict(idx_from=list(), idx_to=list())
self._process_pixels()
self._process_palette()
self._adjust_pixels()
self._adjust_palette()
self._img_p.info['transparency'] = 0
self._img_p.info['background'] = 0
return self._img_p
def _create_animated_gif(images: List[Image], durations: Union[int, List[int]]) -> Tuple[Image, dict]:
"""If the image is a GIF, create an its thumbnail here."""
save_kwargs = dict()
new_images: List[Image] = []
for frame in images:
thumbnail = frame.copy() # type: Image
thumbnail_rgba = thumbnail.convert(mode='RGBA')
thumbnail_rgba.thumbnail(size=frame.size, reducing_gap=3.0)
converter = TransparentAnimatedGifConverter(img_rgba=thumbnail_rgba)
thumbnail_p = converter.process() # type: Image
new_images.append(thumbnail_p)
output_image = new_images[0]
save_kwargs.update(
format='GIF',
save_all=True,
optimize=False,
append_images=new_images[1:],
duration=durations,
disposal=2, # Other disposals don't work
loop=0)
return output_image, save_kwargs
def save_transparent_gif(images: List[Image], durations: Union[int, List[int]], save_file):
"""Creates a transparent GIF, adjusting to avoid transparency issues that are present in the PIL library
Note that this does NOT work for partial alpha. The partial alpha gets discarded and replaced by solid colors.
Parameters:
images: a list of PIL Image objects that compose the GIF frames
durations: an int or List[int] that describes the animation durations for the frames of this GIF
save_file: A filename (string), pathlib.Path object or file object. (This parameter corresponds
and is passed to the PIL.Image.save() method.)
Returns:
Image - The PIL Image object (after first saving the image to the specified target)
"""
root_frame, save_args = _create_animated_gif(images, durations)
root_frame.save(save_file, **save_args)
我正在尝试制作以下图标旋转的 gif:
为了尝试实现这一点,我 运行 使用 Pillow 库 Python 中的以下代码:
from PIL import Image, ImageDraw
images = []
icon = Image.open("cameraIcon.png")
for i in range(0, 360):
images.append(icon.rotate(i))
images[0].save('loadingIcon.gif', format='GIF', save_all=True, append_images=images[1:], duration=40, loop=0, transparency=0)
但是这段代码的输出与我想要的不匹配,我得到了以下 gif :
我不知道发生了什么,希望得到任何帮助,谢谢。
我在使用 PIL 时也遇到了问题,所以这里有一个 ImageMagick 解决方案,让您在我处理它的同时继续前进...
for ((i=0;i<360;i+=5)) ; do
magick camera.png -virtual-pixel transparent -distort SRT "41 41 $i" miff:-
done | magick -background none -dispose background -delay 20 miff:- anim.gif
这里有两个主要问题。
- 您没有指定
Image.save()
的disposal
参数。此参数控制前一帧相对于背景的布置方式。如果未指定,则前一帧“逗留”到下一帧。添加disposal=2
应该可以解决该特定问题。 - 其次,截至撰写本文时,pillow 库存在一个 long-standing 问题,会影响透明度。在许多情况下,它会导致透明区域被黑色像素取代。 issue 4640 of the pillow Github repository. There is a workaround that has been provided by one user in related issue 4644 中描述了该问题。只要您不需要支持部分 alpha 透明度,此解决方法就可以正常工作。我发现它会使 alpha 变平并且在那种情况下不起作用。
以下是我使用的代码(改编自问题 4644 中的代码),希望对您有所帮助。您只需使用图像和持续时间调用 save_transparent_gif
函数即可。
# This code adapted from https://github.com/python-pillow/Pillow/issues/4644 to resolve an issue
# described in https://github.com/python-pillow/Pillow/issues/4640
#
# There is a long-standing issue with the Pillow library that messes up GIF transparency by replacing the
# transparent pixels with black pixels (among other issues) when the GIF is saved using PIL.Image.save().
# This code works around the issue and allows us to properly generate transparent GIFs.
from typing import Tuple, List, Union
from collections import defaultdict
from random import randrange
from itertools import chain
from PIL.Image import Image
class TransparentAnimatedGifConverter(object):
_PALETTE_SLOTSET = set(range(256))
def __init__(self, img_rgba: Image, alpha_threshold: int = 0):
self._img_rgba = img_rgba
self._alpha_threshold = alpha_threshold
def _process_pixels(self):
"""Set the transparent pixels to the color 0."""
self._transparent_pixels = set(
idx for idx, alpha in enumerate(
self._img_rgba.getchannel(channel='A').getdata())
if alpha <= self._alpha_threshold)
def _set_parsed_palette(self):
"""Parse the RGB palette color `tuple`s from the palette."""
palette = self._img_p.getpalette()
self._img_p_used_palette_idxs = set(
idx for pal_idx, idx in enumerate(self._img_p_data)
if pal_idx not in self._transparent_pixels)
self._img_p_parsedpalette = dict(
(idx, tuple(palette[idx * 3:idx * 3 + 3]))
for idx in self._img_p_used_palette_idxs)
def _get_similar_color_idx(self):
"""Return a palette index with the closest similar color."""
old_color = self._img_p_parsedpalette[0]
dict_distance = defaultdict(list)
for idx in range(1, 256):
color_item = self._img_p_parsedpalette[idx]
if color_item == old_color:
return idx
distance = sum((
abs(old_color[0] - color_item[0]), # Red
abs(old_color[1] - color_item[1]), # Green
abs(old_color[2] - color_item[2]))) # Blue
dict_distance[distance].append(idx)
return dict_distance[sorted(dict_distance)[0]][0]
def _remap_palette_idx_zero(self):
"""Since the first color is used in the palette, remap it."""
free_slots = self._PALETTE_SLOTSET - self._img_p_used_palette_idxs
new_idx = free_slots.pop() if free_slots else \
self._get_similar_color_idx()
self._img_p_used_palette_idxs.add(new_idx)
self._palette_replaces['idx_from'].append(0)
self._palette_replaces['idx_to'].append(new_idx)
self._img_p_parsedpalette[new_idx] = self._img_p_parsedpalette[0]
del(self._img_p_parsedpalette[0])
def _get_unused_color(self) -> tuple:
""" Return a color for the palette that does not collide with any other already in the palette."""
used_colors = set(self._img_p_parsedpalette.values())
while True:
new_color = (randrange(256), randrange(256), randrange(256))
if new_color not in used_colors:
return new_color
def _process_palette(self):
"""Adjust palette to have the zeroth color set as transparent. Basically, get another palette
index for the zeroth color."""
self._set_parsed_palette()
if 0 in self._img_p_used_palette_idxs:
self._remap_palette_idx_zero()
self._img_p_parsedpalette[0] = self._get_unused_color()
def _adjust_pixels(self):
"""Convert the pixels into their new values."""
if self._palette_replaces['idx_from']:
trans_table = bytearray.maketrans(
bytes(self._palette_replaces['idx_from']),
bytes(self._palette_replaces['idx_to']))
self._img_p_data = self._img_p_data.translate(trans_table)
for idx_pixel in self._transparent_pixels:
self._img_p_data[idx_pixel] = 0
self._img_p.frombytes(data=bytes(self._img_p_data))
def _adjust_palette(self):
"""Modify the palette in the new `Image`."""
unused_color = self._get_unused_color()
final_palette = chain.from_iterable(
self._img_p_parsedpalette.get(x, unused_color) for x in range(256))
self._img_p.putpalette(data=final_palette)
def process(self) -> Image:
"""Return the processed mode `P` `Image`."""
self._img_p = self._img_rgba.convert(mode='P')
self._img_p_data = bytearray(self._img_p.tobytes())
self._palette_replaces = dict(idx_from=list(), idx_to=list())
self._process_pixels()
self._process_palette()
self._adjust_pixels()
self._adjust_palette()
self._img_p.info['transparency'] = 0
self._img_p.info['background'] = 0
return self._img_p
def _create_animated_gif(images: List[Image], durations: Union[int, List[int]]) -> Tuple[Image, dict]:
"""If the image is a GIF, create an its thumbnail here."""
save_kwargs = dict()
new_images: List[Image] = []
for frame in images:
thumbnail = frame.copy() # type: Image
thumbnail_rgba = thumbnail.convert(mode='RGBA')
thumbnail_rgba.thumbnail(size=frame.size, reducing_gap=3.0)
converter = TransparentAnimatedGifConverter(img_rgba=thumbnail_rgba)
thumbnail_p = converter.process() # type: Image
new_images.append(thumbnail_p)
output_image = new_images[0]
save_kwargs.update(
format='GIF',
save_all=True,
optimize=False,
append_images=new_images[1:],
duration=durations,
disposal=2, # Other disposals don't work
loop=0)
return output_image, save_kwargs
def save_transparent_gif(images: List[Image], durations: Union[int, List[int]], save_file):
"""Creates a transparent GIF, adjusting to avoid transparency issues that are present in the PIL library
Note that this does NOT work for partial alpha. The partial alpha gets discarded and replaced by solid colors.
Parameters:
images: a list of PIL Image objects that compose the GIF frames
durations: an int or List[int] that describes the animation durations for the frames of this GIF
save_file: A filename (string), pathlib.Path object or file object. (This parameter corresponds
and is passed to the PIL.Image.save() method.)
Returns:
Image - The PIL Image object (after first saving the image to the specified target)
"""
root_frame, save_args = _create_animated_gif(images, durations)
root_frame.save(save_file, **save_args)