Search code examples
pythonpython-3.xpython-imaging-librarygif

How can i make my cropped gif corners transparent with python's pillow?


I'm trying to edit an avatar image from my discord users with a GaussianBlur filter, crop it to a circle and overlay an image. So all of these things work, but the cropped gifs have corners, I don't want them. It should be transparent. I searched a lot for a solution but can't find it. I'm inexperienced in coding with python's pillow library and don't know how I could fix it.

My current code transforms this (PICTURE 1):

enter image description here

into this (PICTURE 2):

enter image description here

but it should be this (working for static images but GIFs should keep their animation at the end):

enter image description here

Like you can see my cropped GIF image has white corners. It doesn't contain them if PICTURE 1 is a PNG. So how can I remove these white corners?

And that is my currently used code:

    def crop_center(pil_img, crop_width, crop_height):
        img_width, img_height = pil_img.size
        return pil_img.crop(((img_width - crop_width) // 2,
                             (img_height - crop_height) // 2,
                             (img_width + crop_width) // 2,
                             (img_height + crop_height) // 2))
    
    def crop_max_square(pil_img):
        return crop_center(pil_img, min(pil_img.size), min(pil_img.size))
    
    def mask_circle_transparent(pil_img, blur_radius, offset=0):
        offset = blur_radius * 2 + offset
        mask = Image.new("L", pil_img.size, 0)
        draw = ImageDraw.Draw(mask)
        draw.ellipse((offset, offset, pil_img.size[0] - offset, pil_img.size[1] - offset), fill=255)
        mask = mask.filter(ImageFilter.GaussianBlur(blur_radius))
    
        result = pil_img.copy()
        result.putalpha(mask)
    
        return result


    async def on_message(message):

       if ".gif" in str(message.author.avatar_url):

          frames = []

          gif = Image.open(BytesIO(await message.author.avatar_url.read()))

          for frame in ImageSequence.Iterator(gif):
              frame = frame.copy()
              frame = frame.convert("RGB")
              frame = frame.resize((256, 256))
              frame = mask_circle_transparent(frame, 4)

              background = background.resize((256, 256))

              frame.paste(background, (0, 0), background)
              frames.append(frame)

              print(str(frames))
              frames[0].save('temp_images/result.gif', save_all=True, append_images=frames[1:])

       else:

          im_square = crop_max_square(Image.open(BytesIO(await message.author.avatar_url.read())).convert("RGB"))
          im_square = im_square.resize((256, 256))
          im_thumb = mask_circle_transparent(im_square, 4)

          background = background.resize((256, 256))

          im_thumb.paste(background, (0, 0), background)
          im_thumb.show()

          im_thumb.save('temp_images/result.png')

Edit: I tested a lot now and I think frame = mask_circle_transparent(frame, 4) is the issue here, before I call that function is my frame is blank, just a background. I don't know why. And apparently, the corners aren't white, it's a color from the image background or something like this? I don't know.


Solution

  • Making the smooth edges is not possible using the GIF format. GIF does not have an alpha channel, so partially transparency is not supported (source:How to get better transparency with GIFs?). This means that the result you achieved on the static image cannot be achieved on a moving GIF.

    However, the background in the GIF can be made transparent using this answer: transparent background in gif using Python Imageio. Below you find the resulting GIF and the code to achieve this result. (I had to skip a few parts of your code, since I did not have the background image and the Discord connection).

    transparent lizard

    from PIL import Image, ImageDraw, ImageFilter, ImageSequence
    import numpy as np
    
    def crop_center(pil_img, crop_width, crop_height):
        img_width, img_height = pil_img.size
        return pil_img.crop(((img_width - crop_width) // 2,
                             (img_height - crop_height) // 2,
                             (img_width + crop_width) // 2,
                             (img_height + crop_height) // 2))
    
    def crop_max_square(pil_img):
        return crop_center(pil_img, min(pil_img.size), min(pil_img.size))
    
    def mask_circle_transparent(pil_img, blur_radius, offset=0):
        offset = blur_radius * 2 + offset
        mask = Image.new("L", pil_img.size, 0)
        draw = ImageDraw.Draw(mask)
        draw.ellipse((offset, offset, pil_img.size[0] - offset, pil_img.size[1] - offset), fill=255)
        mask = mask.filter(ImageFilter.GaussianBlur(blur_radius))
    
        result = pil_img.copy()
        result.putalpha(mask)
    
        return result
    
    
    def transparent_frame(im):
        # im = Image.open(path)
        alpha = im.getchannel('A')
    
        # Convert the image into P mode but only use 255 colors in the palette out of 256
        im = im.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=255)
    
        # Set all pixel values below 128 to 255 , and the rest to 0
        mask = Image.eval(alpha, lambda a: 255 if a <=128 else 0)
    
        # Paste the color of index 255 and use alpha as a mask
        im.paste(255, mask)
    
        # The transparency index is 255
        im.info['transparency'] = 255
    
        return im
    
    
    def on_message(message):
    
       # if ".gif" in str(message.author.avatar_url):
    
          frames = []
    
          gif = Image.open('a.gif')
    
          for frame in ImageSequence.Iterator(gif):
              frame = frame.copy()
              frame = frame.convert("RGB")
              frame = frame.resize((256, 256))
              frame = mask_circle_transparent(frame, 4)
    
              # I did not have the background image that you used, so i skipped this part
              # background = background.resize((256, 256))
              # frame.paste(background, (0, 0), background)
              
              # make transparent using the transparent_frame() function
              frame = transparent_frame(frame)
              frames.append(frame)
    
              # print(str(frames))
              frames[0].save('result.gif', save_all=True, append_images=frames[1:], optimize=False) # optimize must be false to preserve the transparent channel
    
       # else:
    
       #    im_square = crop_max_square(Image.open(BytesIO(await message.author.avatar_url.read())).convert("RGB"))
       #    im_square = im_square.resize((256, 256))
       #    im_thumb = mask_circle_transparent(im_square, 4)
    
       #    background = background.resize((256, 256))
    
       #    im_thumb.paste(background, (0, 0), background)
       #    im_thumb.show()
    
       #    im_thumb.save('temp_images/result.png')
       
    if __name__ == "__main__":
        on_message('hi')