-1

Is there a script for cropping gifs in python like this page: https://www.iloveimg.com/crop-image ?

Some time ago, I found Image Cropping using Python but the problem is you need to draw the rectangle with the cursor.

And I need a GUI like https://www.iloveimg.com/crop-image which have a rectangle that it can be moved wherever I want:

ILOVEIMG

See that https://www.iloveimg.com/crop-image crops the GIF into a new animated one. And Image Cropping using Python only crops the first frame of the GIF.

Some modules that I can use are:

  • Tkinter (preferably)
  • Pygame
  • Pillow / PIL
  • Other
martineau
  • 119,623
  • 25
  • 170
  • 301
lvvittor
  • 55
  • 6

2 Answers2

0

After reading some tutorials I came up with this solution:

import numpy as np
import matplotlib.pyplot as plt

from PIL import Image, ImageSequence
from matplotlib.widgets import RectangleSelector

class ImageCutter:
    def __init__(self, file):
        self.file = file
        self.img = Image.open(file)
        self.frames = [np.array(frame.copy().convert("RGB"))
                        for frame in ImageSequence.Iterator(self.img)]

        self.pos = np.array([0,0,0,0])


    def crop(self):
        self.pos = self.pos.astype(int)
        self.cropped_imgs =  [frame[self.pos[1]:self.pos[3], self.pos[0]:self.pos[2]]
                for frame in self.frames]
        self.save()

    def save(self):
        self.imgs_pil = [Image.fromarray(np.uint8(img))
                         for img in self.cropped_imgs]
        self.imgs_pil[0].save(self.file+"_cropped.gif",
                     save_all=True,
                     append_images=self.imgs_pil[1:],
                     duration=16,
                     loop=0)


data = ImageCutter("final.gif")

fig, ax = plt.subplots(1)
ax.axis("off")

plt.imshow(data.frames[0])

def onselect(eclick, erelease):
    "eclick and erelease are matplotlib events at press and release."
    data.pos = np.array([eclick.xdata, eclick.ydata, erelease.xdata, erelease.ydata])

def onrelease(event):
    data.crop()

cid = fig.canvas.mpl_connect('button_release_event', onrelease)
RS = RectangleSelector(ax, onselect, drawtype='box')

You put your filename in an ImageCutter instance and it will plot the first frame, letting you select a box with your mouse, which defines the area to crop. After you defined the area, click somewhere in the image and the programm will save the cropped gif in your working folder.

Instead of the widget, you can then just use your own Rectangle

user8408080
  • 2,428
  • 1
  • 10
  • 19
  • Is there a way to have a RectangleSelector with the same pixels? For example, instead of using the mouse the window should have a rectangle with 500x600 pixels to crop the image. – lvvittor Jan 16 '19 at 01:58
0

Since this is still the top hit on google for cropping gifs in python, it is probably worth an update.

If we generalize the approach from above like this, then the usage is a bit more familiar:

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))

The crop method here will return a PIL image just like Image.crop does.

Usage looks like this:

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))

If you're in a hurry, ignore this part and copy the code above

Another option (just for fun, the first one I present is probably cleaner) would be to monkey-patch the open function from PIL and recurse our crop method like this:

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)

This gives the illusion that we are actually working with the PIL Image class.. this is dangerous unless you plan to override all the other Image methods as well. For most use cases, the first code block I gave should suffice

kibble
  • 36
  • 1
  • 2