조회 수 1100 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄 첨부
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 할 수 있도록 구현해보겠습니다 ^^



List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
38 코드 [Python] 네이버 실시간 검색어 3 title: 황금 서버 (30일)humit 2020.01.23 1123
37 코드 [Python] 네이버 모바일 이미지 검색에서의 이미지 파일을 멀티스레드로 다운받고 1개의 파일로 병합 11 file 이니스프리 2019.07.12 1376
36 코드 [Python] url 주소로부터 IP 주소 알아내기 title: 황금 서버 (30일)humit 2020.02.20 2059
35 코드 [Python] Selenium을 이용하여 특정 element를 캡처하는 스크립트 2 file 이니스프리 2019.07.03 5905
» 코드 [Python] PIL을 이용한 Animated GIF의 리사이징 file 이니스프리 2019.11.03 1100
33 코드 [Python] Google Image Search 결과를 받아오기 file 이니스프리 2019.12.09 947
32 코드 [Python/Telegram] Studyforus 알림봇 (댓글, 스티커 파싱) 7 file 이니스프리 2020.05.15 647
31 코드 [Python-Gnuboard] 파이썬으로 구현한 그누보드 자동 글쓰기 함수 1 file 이니스프리 2021.04.08 1219
30 코드 [PyQt] sir.kr에서 스크랩한 게시글을 보여주는 윈도우앱 (검색 및 정렬 가능) 7 file 이니스프리 2019.08.09 998
29 코드 [PHP] 이미지를 원하는 크기(원본비율 유지)로 리사이즈 하여 출력 (원본 이미지는 수정하지 않습니다) 6 이니스프리 2018.12.20 7706
28 코드 [PHP] 기상청 중기예보를 캐러셀로 보여주는 위젯 (매우 허접합니다 ㅠㅠ) 10 file 이니스프리 2018.09.28 647
27 코드 [PHP] 기상청 RSS 시간별 예보 위젯 - cache 적용(?) 9 file 이니스프리 2018.10.28 850
26 코드 [PHP] 그누보드 자동 게시글 작성 - 일본기상협회의 우리나라 날씨를 크롤링한 후 파파고로 번역하여 글 작성 4 file 이니스프리 2018.11.15 655
25 코드 [PHP] 간단한 캐싱 클래스 3 title: 황금 서버 (30일)humit 2018.12.06 605
24 코드 [PHP/Javascript] 아미나에 자동으로 게시글을 생성하고 Ajax로 전송하여 결과를 표시하기 2 file 이니스프리 2019.07.09 775
23 코드 [JS]클라이언트에서 Ip를 얻어보자 2 Hanam09 2019.01.21 625
22 코드 [JS] 클라이언트단 GET Parameter Hanam09 2019.11.16 465
21 코드 [JS] http를 https로 리디렉션! 3 Hanam09 2018.12.30 674
20 자료 [Bootstrap] xeACE 레이아웃 3 title: 은메달도다 2017.09.17 632
19 자료 [Autohotkey] 매분 정각에 전체화면을 캡쳐하는 스크립트 4 file 이니스프리 2020.05.22 1102
Board Pagination Prev 1 2 3 4 Next
/ 4