Pillow - 调整 GIF 的大小
Pillow - Resizing a GIF
我有一个 gif
,我想用 pillow
调整它的大小,以减小它的大小。 gif
的当前大小为 2MB。
我正在尝试
调整它的大小,使其高度/宽度更小
降低其质量。
对于 JPEG,下面的一段代码通常足以使大图像的尺寸急剧减小。
from PIL import Image
im = Image.open("my_picture.jpg")
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS) # decreases width and height of the image
im.save("out.jpg", optimize=True, quality=85) # decreases its quality
但是,对于 GIF,它似乎不起作用。以下代码甚至使 out.gif
比初始 gif 更大:
im = Image.open("my_gif.gif")
im.seek(im.tell() + 1) # loads all frames
im.save("out.gif", save_all=True, optimize=True, quality=10) # should decrease its quality
print(os.stat("my_gif.gif").st_size) # 2096558 bytes / roughly 2MB
print(os.stat("out.gif").st_size) # 7536404 bytes / roughly 7.5MB
如果我添加以下行,则仅保存 GIF 的第一帧,而不是其所有帧。
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS) # should decrease its size
我一直在考虑在 im.seek()
或 im.tell()
上调用 resize()
但这些方法 return 都不是 Image 对象,因此我无法调用 resize()
在他们的输出上。
您知道我如何使用 Pillow 来减小我的 GIF 的大小同时保留其所有帧吗?
[编辑] 部分解决方案:
继之后,我做了以下改动:
我正在使用 BigglesZX's script 提取所有帧。值得注意的是,这是一个 Python 2 脚本,我的项目是用 Python 3 编写的(我最初确实提到了这个细节,但它被 Stack Overflow 社区编辑掉了)。 运行 2to3 -w gifextract.py
使该脚本与 Python 3.
兼容
我一直在单独调整每一帧:frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)
我一直把所有的帧保存在一起:img.save("out.gif", save_all=True, optimize=True)
。
新 gif 现已保存并可以使用,但存在 2 个主要问题:
我不确定调整大小的方法是否有效,因为 out.gif
仍然是 7.5MB。初始 gif 为 2MB.
gif速度提升,gif不循环。它在第一个 运行.
后停止
示例:
原始 gif my_gif.gif
:
处理后的 Gif (out.gif
) https://i.imgur.com/zDO4cE4.mp4(我无法将其添加到 Stack Overflow )。 Imgur 让它变慢(并将其转换为 mp4)。当我从我的电脑打开gif文件时,整个gif持续大约1.5秒。
根据 Pillow 4.0x,Image.resize 函数仅适用于单个 image/frame。
要实现您想要的效果,我相信您必须首先从 .gif 文件中提取每一帧,一次调整每一帧的大小,然后重新assemble 重新调整它们。
要完成第一步,似乎需要注意一些细节。例如。每个 gif 帧是使用局部调色板还是全局调色板应用于所有帧,以及 gif 是否使用完整或部分帧替换每个图像。 BigglesZX 开发了一个脚本来解决这些问题,同时从 gif 文件中提取每一帧,以便利用它。
接下来,您必须编写脚本来调整每个提取的帧的大小,并使用 PIL assemble 将它们全部作为新的 .gif。Image.resize() 和 PIL.Image.save ().
我注意到你写了“im.seek(im.tell() + 1) # load all frames
”。我认为这是不正确的。相反,它用于在 .gif 文件的帧之间递增。我注意到您在 .gif 文件的保存功能中使用了 quality=10。我没有找到 PIL documentation. You can learn more about the tile attribute mentioned in BiggleZX's script by reading this link
中提供的这个
使用 BigglesZX's script,我创建了一个使用 Pillow 调整 GIF 大小的新脚本。
原始 GIF (2.1 MB):
调整大小后输出 GIF (1.7 MB):
我已经保存了脚本here。它使用 Pillow 的 thumbnail
方法而不是 resize
方法,因为我发现 resize
方法不起作用。
它并不完美,请随意分叉和改进它。以下是一些未解决的问题:
- 虽然 GIF 在由 imgur 托管时显示得很好,但当我从我的计算机打开它时存在速度问题,整个 GIF 只需要 1.5 秒。
- 同样,虽然 imgur 似乎弥补了速度问题,但当我尝试将其上传到
stack.imgur
时,GIF 无法正确显示。只显示了第一帧(可以看到here)。
完整代码(应该删除上面的要点):
def resize_gif(path, save_as=None, resize_to=None):
"""
Resizes the GIF to a given length:
Args:
path: the path to the GIF file
save_as (optional): Path of the resized gif. If not set, the original gif will be overwritten.
resize_to (optional): new size of the gif. Format: (int, int). If not set, the original GIF will be resized to
half of its size.
"""
all_frames = extract_and_resize_frames(path, resize_to)
if not save_as:
save_as = path
if len(all_frames) == 1:
print("Warning: only 1 frame found")
all_frames[0].save(save_as, optimize=True)
else:
all_frames[0].save(save_as, optimize=True, save_all=True, append_images=all_frames[1:], loop=1000)
def analyseImage(path):
"""
Pre-process pass over the image to determine the mode (full or additive).
Necessary as assessing single frames isn't reliable. Need to know the mode
before processing all frames.
"""
im = Image.open(path)
results = {
'size': im.size,
'mode': 'full',
}
try:
while True:
if im.tile:
tile = im.tile[0]
update_region = tile[1]
update_region_dimensions = update_region[2:]
if update_region_dimensions != im.size:
results['mode'] = 'partial'
break
im.seek(im.tell() + 1)
except EOFError:
pass
return results
def extract_and_resize_frames(path, resize_to=None):
"""
Iterate the GIF, extracting each frame and resizing them
Returns:
An array of all frames
"""
mode = analyseImage(path)['mode']
im = Image.open(path)
if not resize_to:
resize_to = (im.size[0] // 2, im.size[1] // 2)
i = 0
p = im.getpalette()
last_frame = im.convert('RGBA')
all_frames = []
try:
while True:
# print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))
'''
If the GIF uses local colour tables, each frame will have its own palette.
If not, we need to apply the global palette to the new frame.
'''
if not im.getpalette():
im.putpalette(p)
new_frame = Image.new('RGBA', im.size)
'''
Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
If so, we need to construct the new frame by pasting it on top of the preceding frames.
'''
if mode == 'partial':
new_frame.paste(last_frame)
new_frame.paste(im, (0, 0), im.convert('RGBA'))
new_frame.thumbnail(resize_to, Image.ANTIALIAS)
all_frames.append(new_frame)
i += 1
last_frame = new_frame
im.seek(im.tell() + 1)
except EOFError:
pass
return all_frames
我正在使用下面的函数调整大小和裁剪图像,包括动画图像(GIF、WEBP)简单地说,我们需要迭代 gif 或 webp 中的每一帧。
from math import floor, fabs
from PIL import Image, ImageSequence
def transform_image(original_img, crop_w, crop_h):
"""
Resizes and crops the image to the specified crop_w and crop_h if necessary.
Works with multi frame gif and webp images also.
args:
original_img is the image instance created by pillow ( Image.open(filepath) )
crop_w is the width in pixels for the image that will be resized and cropped
crop_h is the height in pixels for the image that will be resized and cropped
returns:
Instance of an Image or list of frames which they are instances of an Image individually
"""
img_w, img_h = (original_img.size[0], original_img.size[1])
n_frames = getattr(original_img, 'n_frames', 1)
def transform_frame(frame):
"""
Resizes and crops the individual frame in the image.
"""
# resize the image to the specified height if crop_w is null in the recipe
if crop_w is None:
if crop_h == img_h:
return frame
new_w = floor(img_w * crop_h / img_h)
new_h = crop_h
return frame.resize((new_w, new_h))
# return the original image if crop size is equal to img size
if crop_w == img_w and crop_h == img_h:
return frame
# first resize to get most visible area of the image and then crop
w_diff = fabs(crop_w - img_w)
h_diff = fabs(crop_h - img_h)
enlarge_image = True if crop_w > img_w or crop_h > img_h else False
shrink_image = True if crop_w < img_w or crop_h < img_h else False
if enlarge_image is True:
new_w = floor(crop_h * img_w / img_h) if h_diff > w_diff else crop_w
new_h = floor(crop_w * img_h / img_w) if h_diff < w_diff else crop_h
if shrink_image is True:
new_w = crop_w if h_diff > w_diff else floor(crop_h * img_w / img_h)
new_h = crop_h if h_diff < w_diff else floor(crop_w * img_h / img_w)
left = (new_w - crop_w) // 2
right = left + crop_w
top = (new_h - crop_h) // 2
bottom = top + crop_h
return frame.resize((new_w, new_h)).crop((left, top, right, bottom))
# single frame image
if n_frames == 1:
return transform_frame(original_img)
# in the case of a multiframe image
else:
frames = []
for frame in ImageSequence.Iterator(original_img):
frames.append( transform_frame(frame) )
return frames
我尝试使用所选答案中给出的脚本,但正如 Pauline 评论的那样,它存在一些问题,例如速度问题。
问题是保存新 gif 时没有给出速度。要解决这个问题,您必须从原始 gif 中获取速度并在保存时将其传递给新的。
这是我的脚本:
from PIL import Image
def scale_gif(path, scale, new_path=None):
gif = Image.open(path)
if not new_path:
new_path = path
old_gif_information = {
'loop': bool(gif.info.get('loop', 1)),
'duration': gif.info.get('duration', 40),
'background': gif.info.get('background', 223),
'extension': gif.info.get('extension', (b'NETSCAPE2.0')),
'transparency': gif.info.get('transparency', 223)
}
new_frames = get_new_frames(gif, scale)
save_new_gif(new_frames, old_gif_information, new_path)
def get_new_frames(gif, scale):
new_frames = []
actual_frames = gif.n_frames
for frame in range(actual_frames):
gif.seek(frame)
new_frame = Image.new('RGBA', gif.size)
new_frame.paste(gif)
new_frame.thumbnail(scale, Image.ANTIALIAS)
new_frames.append(new_frame)
return new_frames
def save_new_gif(new_frames, old_gif_information, new_path):
new_frames[0].save(new_path,
save_all = True,
append_images = new_frames[1:],
duration = old_gif_information['duration'],
loop = old_gif_information['loop'],
background = old_gif_information['background'],
extension = old_gif_information['extension'] ,
transparency = old_gif_information['transparency'])
我还注意到您必须使用 new_frames[0]
保存新的 gif,而不是创建新的 Image Pillow 对象,以避免向 gif 添加黑框。
如果您想在此脚本上使用 pytest 查看测试,您可以查看 my GitHub's repo。
我有一个 gif
,我想用 pillow
调整它的大小,以减小它的大小。 gif
的当前大小为 2MB。
我正在尝试
调整它的大小,使其高度/宽度更小
降低其质量。
对于 JPEG,下面的一段代码通常足以使大图像的尺寸急剧减小。
from PIL import Image
im = Image.open("my_picture.jpg")
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS) # decreases width and height of the image
im.save("out.jpg", optimize=True, quality=85) # decreases its quality
但是,对于 GIF,它似乎不起作用。以下代码甚至使 out.gif
比初始 gif 更大:
im = Image.open("my_gif.gif")
im.seek(im.tell() + 1) # loads all frames
im.save("out.gif", save_all=True, optimize=True, quality=10) # should decrease its quality
print(os.stat("my_gif.gif").st_size) # 2096558 bytes / roughly 2MB
print(os.stat("out.gif").st_size) # 7536404 bytes / roughly 7.5MB
如果我添加以下行,则仅保存 GIF 的第一帧,而不是其所有帧。
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS) # should decrease its size
我一直在考虑在 im.seek()
或 im.tell()
上调用 resize()
但这些方法 return 都不是 Image 对象,因此我无法调用 resize()
在他们的输出上。
您知道我如何使用 Pillow 来减小我的 GIF 的大小同时保留其所有帧吗?
[编辑] 部分解决方案:
继
我正在使用 BigglesZX's script 提取所有帧。值得注意的是,这是一个 Python 2 脚本,我的项目是用 Python 3 编写的(我最初确实提到了这个细节,但它被 Stack Overflow 社区编辑掉了)。 运行
2to3 -w gifextract.py
使该脚本与 Python 3. 兼容
我一直在单独调整每一帧:
frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)
我一直把所有的帧保存在一起:
img.save("out.gif", save_all=True, optimize=True)
。
新 gif 现已保存并可以使用,但存在 2 个主要问题:
我不确定调整大小的方法是否有效,因为
out.gif
仍然是 7.5MB。初始 gif 为 2MB.gif速度提升,gif不循环。它在第一个 运行.
后停止
示例:
原始 gif my_gif.gif
:
处理后的 Gif (out.gif
) https://i.imgur.com/zDO4cE4.mp4(我无法将其添加到 Stack Overflow )。 Imgur 让它变慢(并将其转换为 mp4)。当我从我的电脑打开gif文件时,整个gif持续大约1.5秒。
根据 Pillow 4.0x,Image.resize 函数仅适用于单个 image/frame。
要实现您想要的效果,我相信您必须首先从 .gif 文件中提取每一帧,一次调整每一帧的大小,然后重新assemble 重新调整它们。
要完成第一步,似乎需要注意一些细节。例如。每个 gif 帧是使用局部调色板还是全局调色板应用于所有帧,以及 gif 是否使用完整或部分帧替换每个图像。 BigglesZX 开发了一个脚本来解决这些问题,同时从 gif 文件中提取每一帧,以便利用它。
接下来,您必须编写脚本来调整每个提取的帧的大小,并使用 PIL assemble 将它们全部作为新的 .gif。Image.resize() 和 PIL.Image.save ().
我注意到你写了“im.seek(im.tell() + 1) # load all frames
”。我认为这是不正确的。相反,它用于在 .gif 文件的帧之间递增。我注意到您在 .gif 文件的保存功能中使用了 quality=10。我没有找到 PIL documentation. You can learn more about the tile attribute mentioned in BiggleZX's script by reading this link
使用 BigglesZX's script,我创建了一个使用 Pillow 调整 GIF 大小的新脚本。
原始 GIF (2.1 MB):
调整大小后输出 GIF (1.7 MB):
我已经保存了脚本here。它使用 Pillow 的 thumbnail
方法而不是 resize
方法,因为我发现 resize
方法不起作用。
它并不完美,请随意分叉和改进它。以下是一些未解决的问题:
- 虽然 GIF 在由 imgur 托管时显示得很好,但当我从我的计算机打开它时存在速度问题,整个 GIF 只需要 1.5 秒。
- 同样,虽然 imgur 似乎弥补了速度问题,但当我尝试将其上传到
stack.imgur
时,GIF 无法正确显示。只显示了第一帧(可以看到here)。
完整代码(应该删除上面的要点):
def resize_gif(path, save_as=None, resize_to=None):
"""
Resizes the GIF to a given length:
Args:
path: the path to the GIF file
save_as (optional): Path of the resized gif. If not set, the original gif will be overwritten.
resize_to (optional): new size of the gif. Format: (int, int). If not set, the original GIF will be resized to
half of its size.
"""
all_frames = extract_and_resize_frames(path, resize_to)
if not save_as:
save_as = path
if len(all_frames) == 1:
print("Warning: only 1 frame found")
all_frames[0].save(save_as, optimize=True)
else:
all_frames[0].save(save_as, optimize=True, save_all=True, append_images=all_frames[1:], loop=1000)
def analyseImage(path):
"""
Pre-process pass over the image to determine the mode (full or additive).
Necessary as assessing single frames isn't reliable. Need to know the mode
before processing all frames.
"""
im = Image.open(path)
results = {
'size': im.size,
'mode': 'full',
}
try:
while True:
if im.tile:
tile = im.tile[0]
update_region = tile[1]
update_region_dimensions = update_region[2:]
if update_region_dimensions != im.size:
results['mode'] = 'partial'
break
im.seek(im.tell() + 1)
except EOFError:
pass
return results
def extract_and_resize_frames(path, resize_to=None):
"""
Iterate the GIF, extracting each frame and resizing them
Returns:
An array of all frames
"""
mode = analyseImage(path)['mode']
im = Image.open(path)
if not resize_to:
resize_to = (im.size[0] // 2, im.size[1] // 2)
i = 0
p = im.getpalette()
last_frame = im.convert('RGBA')
all_frames = []
try:
while True:
# print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))
'''
If the GIF uses local colour tables, each frame will have its own palette.
If not, we need to apply the global palette to the new frame.
'''
if not im.getpalette():
im.putpalette(p)
new_frame = Image.new('RGBA', im.size)
'''
Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
If so, we need to construct the new frame by pasting it on top of the preceding frames.
'''
if mode == 'partial':
new_frame.paste(last_frame)
new_frame.paste(im, (0, 0), im.convert('RGBA'))
new_frame.thumbnail(resize_to, Image.ANTIALIAS)
all_frames.append(new_frame)
i += 1
last_frame = new_frame
im.seek(im.tell() + 1)
except EOFError:
pass
return all_frames
我正在使用下面的函数调整大小和裁剪图像,包括动画图像(GIF、WEBP)简单地说,我们需要迭代 gif 或 webp 中的每一帧。
from math import floor, fabs
from PIL import Image, ImageSequence
def transform_image(original_img, crop_w, crop_h):
"""
Resizes and crops the image to the specified crop_w and crop_h if necessary.
Works with multi frame gif and webp images also.
args:
original_img is the image instance created by pillow ( Image.open(filepath) )
crop_w is the width in pixels for the image that will be resized and cropped
crop_h is the height in pixels for the image that will be resized and cropped
returns:
Instance of an Image or list of frames which they are instances of an Image individually
"""
img_w, img_h = (original_img.size[0], original_img.size[1])
n_frames = getattr(original_img, 'n_frames', 1)
def transform_frame(frame):
"""
Resizes and crops the individual frame in the image.
"""
# resize the image to the specified height if crop_w is null in the recipe
if crop_w is None:
if crop_h == img_h:
return frame
new_w = floor(img_w * crop_h / img_h)
new_h = crop_h
return frame.resize((new_w, new_h))
# return the original image if crop size is equal to img size
if crop_w == img_w and crop_h == img_h:
return frame
# first resize to get most visible area of the image and then crop
w_diff = fabs(crop_w - img_w)
h_diff = fabs(crop_h - img_h)
enlarge_image = True if crop_w > img_w or crop_h > img_h else False
shrink_image = True if crop_w < img_w or crop_h < img_h else False
if enlarge_image is True:
new_w = floor(crop_h * img_w / img_h) if h_diff > w_diff else crop_w
new_h = floor(crop_w * img_h / img_w) if h_diff < w_diff else crop_h
if shrink_image is True:
new_w = crop_w if h_diff > w_diff else floor(crop_h * img_w / img_h)
new_h = crop_h if h_diff < w_diff else floor(crop_w * img_h / img_w)
left = (new_w - crop_w) // 2
right = left + crop_w
top = (new_h - crop_h) // 2
bottom = top + crop_h
return frame.resize((new_w, new_h)).crop((left, top, right, bottom))
# single frame image
if n_frames == 1:
return transform_frame(original_img)
# in the case of a multiframe image
else:
frames = []
for frame in ImageSequence.Iterator(original_img):
frames.append( transform_frame(frame) )
return frames
我尝试使用所选答案中给出的脚本,但正如 Pauline 评论的那样,它存在一些问题,例如速度问题。
问题是保存新 gif 时没有给出速度。要解决这个问题,您必须从原始 gif 中获取速度并在保存时将其传递给新的。
这是我的脚本:
from PIL import Image
def scale_gif(path, scale, new_path=None):
gif = Image.open(path)
if not new_path:
new_path = path
old_gif_information = {
'loop': bool(gif.info.get('loop', 1)),
'duration': gif.info.get('duration', 40),
'background': gif.info.get('background', 223),
'extension': gif.info.get('extension', (b'NETSCAPE2.0')),
'transparency': gif.info.get('transparency', 223)
}
new_frames = get_new_frames(gif, scale)
save_new_gif(new_frames, old_gif_information, new_path)
def get_new_frames(gif, scale):
new_frames = []
actual_frames = gif.n_frames
for frame in range(actual_frames):
gif.seek(frame)
new_frame = Image.new('RGBA', gif.size)
new_frame.paste(gif)
new_frame.thumbnail(scale, Image.ANTIALIAS)
new_frames.append(new_frame)
return new_frames
def save_new_gif(new_frames, old_gif_information, new_path):
new_frames[0].save(new_path,
save_all = True,
append_images = new_frames[1:],
duration = old_gif_information['duration'],
loop = old_gif_information['loop'],
background = old_gif_information['background'],
extension = old_gif_information['extension'] ,
transparency = old_gif_information['transparency'])
我还注意到您必须使用 new_frames[0]
保存新的 gif,而不是创建新的 Image Pillow 对象,以避免向 gif 添加黑框。
如果您想在此脚本上使用 pytest 查看测试,您可以查看 my GitHub's repo。