코드

[Python] PIL을 이용한 Animated GIF의 리사이징

by 이니스프리 posted Nov 03, 2019
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄
Extra Form
라이선스 기타(따로 작성)


1. 스크립트를 작성하게 된 계기


Animated GIF를 리사이징해보려고 했는데 파이썬이나 PHP로 작성된 스크립트를 찾지 못했습니다.


이러한 스크립트가 흔하지 않은 이유를 생각해보면, 우선 ImageMagick을 사용하면 간단히 해결할 수 있기 때문이겠죠.


또한 JPG나 PNG와 달리 GIF 움짤의 경우에는 일반적으로 커뮤니티 사이트에 올라오는 이미지 사이즈가 너무 커서 


이를 부득이 리사이징해야 되는 경우는 많지 않을 것으로 생각됩니다. 



2. 스크립트


파이썬을 이용한 여러가지 방법이 있겠지만 PIL 라이브러리만 사용했으며, 


그 중에서도 PIL을 이용하여 프레임을 하나씩 변환해주는 방식을 채택했습니다.


이 과정에서 thumbnail 메서드를 사용하는 것이 resize 메서드를 사용하는 것보다 안정적이라고 하더군요.


LANCZOS 필터를 사용하긴 했지만, 다른 필터를 사용해도 큰 차이를 느끼지는 못했습니다.


덮어쓰기가 되지 않도록 파일명을 변환하는 부분과, animated GIF가 아닌 파일을 입력한 경우 등을 처리하는 부분을 제외하면


PIL을 이용하여 리사이징 프로세싱을 하는 스크립트 자체는 간단합니다.



다만 각 프레임을 프로세싱하여 새로운 이미지 파일을 생성하는 과정을 거치기 때문에


기존 파일의 프레임 사이의 duration에 대한 설정이 날아가게 됩니다.


파이썬에서 GIF 파일의 duration 설정값을 바로 알아내는 방법을 찾아내지는 못했고, 


우회적으로 총 러닝타임을 구하여 이를 프레임으로 나누는 방법을 택했습니다.


다만 테스트 결과 약간의 오차가 발생하는 것은 제 실력으로는 어쩔 수 없었습니다 ㅠㅠ



from PIL import Image
import os

def resize_gif(path, save_as=None, resize_to=None, ratio=None):
    all_frames = extract_and_resize_frames(path, resize_to, ratio)
   
    im = Image.open(path)
    framerate = get_duration_per_frames(im)

    if not save_as:
        number = 1
        if not '\\' in path:
            path = os.getcwd() + '\\' + path
        while os.path.isfile(path):
            save_as = os.path.splitext(path)[0] + str(number) + '.gif'
            if not os.path.isfile(save_as):
                break
            number += 1
        print('The resized filename will be ' + os.path.basename(save_as))

    if len(all_frames) == 1:
        print("Warning : There is only 1 frame.")
        print('The file was resized to ' + str(all_frames[0].size) + '.')
        all_frames[0].save(save_as, optimize=True)
    else:
        print('The file was resized to ' + str(all_frames[0].size) + '.')
        all_frames[0].save(save_as, optimize=True, save_all=True, append_images=all_frames[1:], loop=0, duration=framerate)


def analyseImage(path):
    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
    finally:
        return results


def extract_and_resize_frames(path, resize_to=None, ratio=None):
    mode = analyseImage(path)['mode']
    im = Image.open(path)
    if not resize_to:
        if not ratio:
            ratio = 0.5
        resize_to = [int(ratio * s) for s in im.size]
    i = 0
    p = im.getpalette()
    last_frame = im.convert('RGBA')
    all_frames = []

    try:
        while True:
            if not im.getpalette():
                im.putpalette(p)
            new_frame = Image.new('RGBA', im.size)
            if mode == 'partial':
                new_frame.paste(last_frame)
            new_frame.paste(im, (0, 0), im.convert('RGBA'))
            new_frame.thumbnail(resize_to, Image.LANCZOS)
            all_frames.append(new_frame)
            i += 1
            last_frame = new_frame
            im.seek(im.tell() + 1)
    except EOFError:
        pass
    finally:
        return all_frames


def get_duration_per_frames(image_object):
    image_object.seek(0)
    duration = 0
    try:
        while True:
            duration += image_object.info['duration']
            image_object.seek(image_object.tell() + 1)
    except EOFError:
        pass
    finally:
        duration_per_frames = round(duration / image_object.n_frames, 2)
        return duration_per_frames

resize_gif('파일명.gif', save_as=None, resize_to=None, ratio=None)



3. 사용례

원래 사이즈: 1730kb


절반 사이즈: 1325kb


ratio 값을 따로 입력하지 않으면 위와 같이 절반 사이즈로 축소됩니다.


다만 PIL 라이브러리가 자체적으로 GIF 압축 기능을 제공하지 않기 때문에,


파일 용량은 그다지 줄어들지 않는 점을 확인할 수 있습니다.


어중간하게 80% 정도의 사이즈로 축소를 하면 오히려 파일 용량이 증가하는 문제가 있습니다.


GIFLossy 알고리즘 등을 사용해야 이 문제를 해결할 수 있을 것 같습니다.



한편 duration과 관련된 문제는 http://gifduration.konstochvanligasaker.se/ 에서 확인을 해보면


두 파일 모두 정확히 3840ms를 기록한 것을 확인할 수 있었습니다.



4. 결론


PIL 라이브러리만을 이용하여 GIF 움짤을 리사이징하는 것은 의외로 간단히 해결되지만, 


(1) GIF 파일의 duration 설정값이 변하는 문제가 있으며, 


(2) 파일 용량을 확실히 감소시킬 목적으로는 GIFLossy를 이용하는 방법을 권장합니다.


다음에 시간이 날 때 PyQt를 이용하여 파일을 Drag & Drop 할 수 있도록 구현해보겠습니다 ^^



Articles

1 2 3 4