由于这仍然是谷歌在 python 中裁剪 gif 的热门话题,因此可能值得更新。
如果我们像这样概括上面的方法,那么用法就更熟悉了:
import io
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple, Union
import numpy as np
from PIL import Image, ImageSequence
Left, Upper, Right, Lower = int, int, int, int
Box = Tuple[Left, Upper, Right, Lower]
Frames = List[np.ndarray]
ImageArray = List[Image.Image]
File = Union[str, bytes, Path, io.BytesIO]
@dataclass
class MultiFrameImage:
fp: File
@property
def im(self):
return Image.open(self.fp)
@property
def frames(self) -> Frames:
return [
np.array(frame.copy().convert("RGB"))
for frame in ImageSequence.Iterator(self.im)
]
def crop_frames(self, box: Box) -> List[np.ndarray]:
left, upper, right, lower = box
return [frame[upper:lower, left:right] for frame in self.frames]
def image_array_from_frames(self, frames: Frames) -> ImageArray:
return [Image.fromarray(np.uint8(frame)) for frame in frames]
def crop_to_buffer(self, box: Box, **kwargs) -> io.BytesIO:
cropped_frames = self.crop_frames(box)
cropped_images = self.image_array_from_frames(cropped_frames)
buffer = io.BytesIO()
cropped_images[0].save(
buffer,
save_all=True,
format="GIF",
append_images=cropped_images[1:],
duration=16,
loop=0,
**kwargs
)
return buffer
def crop(self, box: Box, **kwargs) -> Image.Image:
return Image.open(self.crop_to_buffer(box, **kwargs))
这里的crop 方法将返回一个PIL 图像,就像Image.crop 一样。
用法如下:
image = MultiFrameImage(io.BytesIO(avatar_bytes))
buffer = image.crop_to_buffer((left, upper, right, lower))
# or if you need the image instead of the buffer
cropped_image = image.crop((left, upper, right, lower))
如果你赶时间,忽略这部分并复制上面的代码
另一种选择(只是为了好玩,我提出的第一个可能更干净)是从 PIL 猴子修补 open 函数并递归我们的 crop 方法,如下所示:
import io
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple, Union, cast
import numpy as np
from PIL import Image, ImageSequence
Left, Upper, Right, Lower = int, int, int, int
Box = Tuple[Left, Upper, Right, Lower]
Frames = List[np.ndarray]
ImageArray = List[Image.Image]
File = Union[str, bytes, Path, io.BytesIO]
@dataclass
class MultiFrameImage:
fp: File
@property
def im(self):
return Image.open(self.fp)
@property
def frames(self) -> Frames:
return [
np.array(frame.copy().convert("RGB"))
for frame in ImageSequence.Iterator(self.im)
]
def crop_frames(self, box: Box) -> List[np.ndarray]:
left, upper, right, lower = box
return [frame[upper:lower, left:right] for frame in self.frames]
def image_array_from_frames(self, frames: Frames) -> ImageArray:
return [Image.fromarray(np.uint8(frame)) for frame in frames]
def crop_to_buffer(self, box: Box, **kwargs) -> io.BytesIO:
cropped_frames = self.crop_frames(box)
cropped_images = self.image_array_from_frames(cropped_frames)
buffer = io.BytesIO()
cropped_images[0].save(
buffer,
save_all=True,
format="GIF",
append_images=cropped_images[1:],
duration=16,
loop=0,
**kwargs
)
return buffer
def crop(self, box: Box, **kwargs) -> "MultiFrameImage":
return open_multiframe_image(self.crop_to_buffer(box, **kwargs))
class MonkeyPatchedMultiFrameImage(Image.Image, MultiFrameImage):
pass
def open_multiframe_image(fp):
multi_frame_im = MultiFrameImage(fp)
im = multi_frame_im.im
setattr(im, "frames", multi_frame_im.frames)
setattr(im, "crop_frames", multi_frame_im.crop_frames)
setattr(im, "image_array_from_frames", multi_frame_im.image_array_from_frames)
setattr(im, "crop_to_buffer", multi_frame_im.crop_to_buffer)
setattr(im, "crop", multi_frame_im.crop)
return cast(MonkeyPatchedMultiFrameImage, im)
这给人一种我们实际上是在使用 PIL Image 类的错觉。这很危险,除非您打算同时覆盖所有其他 Image 方法。对于大多数用例,我给出的第一个代码块就足够了