Python圖片轉gif(將靜態圖轉化爲分塊加載的動態圖)

簡介

將靜態圖轉化爲分塊加載的動態圖

方案

1. PIL: 
    1. 創建背景圖
    2. 將原圖拆分成N塊並依次合成到背景圖的相應位置, 得到N張素材圖
    3. 將N張素材圖合成GIF

2. pygifsicle
    對合成的GIF進行優化(無損壓縮, 精簡體積)
    注意: 需要電腦安裝gifsicle, 官網: https://www.lcdf.org/gifsicle/, 
    若看不懂英文, 網上資料一大把, (其實不安裝也不影響正常使用, 只是沒有優化GIF而已)

3. tkinter:
    用於圖形化界面的實現, 便於操作
    
4. pyinstaller
    用於將腳本打包成exe

源碼

Fork me on Gitee

https://gitee.com/tianshl/img2gif.git

腳本介紹

img2gif.py
簡介: 將圖片轉成gif 命令行模式
使用: python img2gif.py -h  
示例: python img2gif.py -p /Users/tianshl/Documents/sample.jpg
img2gif_gui.py
簡介: 將圖片轉成gif 圖像化界面
使用: python img2gif_gui.py

打包成exe

pyinstaller -F -w -i gif.ico img2gif_gui.py
# 執行完指令後, exe文件在dist目錄下
# 我打包的exe: https://download.csdn.net/download/xiaobuding007/12685554

效果圖

命令行模式

圖形化界面

代碼

requirements.txt (依賴)
Pillow==7.2.0
pygifsicle==1.0.1
img2gif.py (命令行模式 )
# -*- coding: utf-8 -*-
"""
 **********************************************************
 * Author        : tianshl
 * Email         : [email protected]
 * Last modified : 2020-07-29 14:58:57
 * Filename      : img2gif.py
 * Description   : 圖片轉動圖
 * Documents     : https://www.lcdf.org/gifsicle/
 * ********************************************************
"""
import argparse
import copy
import logging
import os
import random

from PIL import Image
from pygifsicle import optimize

LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
log = logging.getLogger(__name__)


class Img2Gif:
    """
    圖片轉動圖
    """

    def __init__(self, img_path, blocks=16, mode='append', random_block=False):
        """
        初始化
        :param img_path:        圖片地址
        :param blocks:          分塊數
        :param mode:            展示模式 append: 追加, flow: 流式, random: 隨機
        :param random_block:    隨機拆分
        """
        self.mode = mode if mode in ['flow', 'append', 'random'] else 'append'

        self.blocks = blocks
        self.random_block = random_block

        # 背景圖
        self.img_background = None

        self.img_path = img_path
        self.img_dir, self.img_name = os.path.split(img_path)
        self.img_name = os.path.splitext(self.img_name)[0]

        self.gif_path = os.path.join(self.img_dir, '{}.gif'.format(self.img_name))

    def get_ranges(self):
        """
        獲取橫向和縱向塊數
        """
        if not self.random_block:
            w = int(self.blocks ** 0.5)
            return w, w

        ranges = list()
        for w in range(2, int(self.blocks ** 0.5) + 1):
            if self.blocks % w == 0:
                ranges.append((w, self.blocks // w))

        if ranges:
            return random.choice(ranges)
        else:
            return self.blocks, 1

    def materials(self):
        """
        素材
        """

        log.info('分割圖片')
        img_origin = Image.open(self.img_path)
        (width, height) = img_origin.size
        self.img_background = Image.new(img_origin.mode, img_origin.size)

        # 單方向分割次數
        blocks_w, blocks_h = self.get_ranges()

        block_width = width // blocks_w
        block_height = height // blocks_h

        img_tmp = copy.copy(self.img_background)
        # 動圖中的每一幀
        _materials = list()
        for h in range(blocks_h):
            for w in range(blocks_w):
                block_box = (w * block_width, h * block_height, (w + 1) * block_width, (h + 1) * block_height)
                block_img = img_origin.crop(block_box)
                if self.mode in ['flow', 'random']:
                    img_tmp = copy.copy(self.img_background)
                img_tmp.paste(block_img, (w * block_width, h * block_height))
                _materials.append(copy.copy(img_tmp))

        # 隨機打亂順序
        if self.mode == 'random':
            random.shuffle(_materials)

        log.info('分割完成')
        # 最後十幀展示原圖
        [_materials.append(copy.copy(img_origin)) for _ in range(10)]
        return _materials

    def gif(self):
        """
        合成gif
        """

        materials = self.materials()
        log.info('合成GIF')
        self.img_background.save(self.gif_path, save_all=True, loop=True, append_images=materials, duration=250)
        log.info('合成完成')

        log.info('壓縮GIF')
        optimize(self.gif_path)
        log.info('壓縮完成')


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-p", "--img_path", required=True, help="圖片路徑")
    parser.add_argument("-b", "--blocks", type=int, default=16, help="塊數")
    parser.add_argument("-r", "--random_block", type=bool, default=False, help="隨機拆分塊數")
    parser.add_argument(
        '-m', '--mode', default='append', choices=['append', 'flow', 'random'],
        help="塊展示模式 append: 追加, flow: 流式, random: 隨機"
    )
    args = parser.parse_args()

    Img2Gif(**args.__dict__).gif()

img2gif_gui.py (圖形化界面)
# -*- coding: utf-8 -*-
"""
 **********************************************************
 * Author        : tianshl
 * Email         : [email protected]
 * Last modified : 2020-07-29 14:58:57
 * Filename      : img2gif_gui.py
 * Description   : 圖片轉動圖
 * Documents     : https://www.lcdf.org/gifsicle/
 * ********************************************************
"""
import copy
import random
from tkinter import *
from tkinter import ttk, messagebox
from tkinter.filedialog import askopenfilename, asksaveasfilename

from PIL import Image, ImageTk
from pygifsicle import optimize


class Img2Gif(Frame):
    """
    圖形化界面
    """

    def __init__(self):
        """
        初始化
        """
        Frame.__init__(self)

        # 設置窗口信息
        self.__set_win_info()

        # 渲染窗口
        self._gif_pane = None
        self.__render_pane()

    def __set_win_info(self):
        """
        設置窗口信息
        """
        # 獲取屏幕分辨率
        win_w = self.winfo_screenwidth()
        win_h = self.winfo_screenheight()
        # 設置窗口尺寸/位置
        self._width = 260
        self._height = 300
        self.master.geometry('{}x{}+{}+{}'.format(
            self._width, self._height, (win_w - self._width) // 2, (win_h - self._height) // 2)
        )
        # 設置窗口不可變
        self.master.resizable(width=False, height=False)

    @staticmethod
    def __destroy_frame(frame):
        """
        銷燬frame
        """
        if frame is None:
            return

        for widget in frame.winfo_children():
            widget.destroy()

        frame.destroy()

    def __render_pane(self):
        """
        渲染窗口
        """

        self._main_pane = Frame(self.master, width=self._width, height=self._height)
        self._main_pane.pack()

        # 設置窗口標題
        self.master.title('圖片轉GIF')

        # 選擇圖片
        image_path_label = Label(self._main_pane, text='選擇圖片', relief=RIDGE, padx=10)
        image_path_label.place(x=10, y=10)

        self._image_path_entry = Entry(self._main_pane, width=13)
        self._image_path_entry.place(x=90, y=7)

        image_path_button = Label(self._main_pane, text='···', relief=RIDGE, padx=5)
        image_path_button.bind('<Button-1>', self.__select_image)
        image_path_button.place(x=220, y=10)

        # 拆分塊數
        blocks_label = Label(self._main_pane, text='拆分塊數', relief=RIDGE, padx=10)
        blocks_label.place(x=10, y=50)

        self._blocks_scale = Scale(
            self._main_pane, from_=2, to=100, orient=HORIZONTAL, sliderlength=10
        )
        self._blocks_scale.set(16)
        self._blocks_scale.place(x=90, y=33)

        Label(self._main_pane, text='(塊)').place(x=200, y=50)

        # 隨機拆分
        random_block_label = Label(self._main_pane, text='隨機拆分', relief=RIDGE, padx=10)
        random_block_label.place(x=10, y=90)

        self._random_block = BooleanVar(value=False)
        random_block_check_button = ttk.Checkbutton(
            self._main_pane, variable=self._random_block,
            width=0, onvalue=True, offvalue=False
        )
        random_block_check_button.place(x=90, y=90)

        # 動圖模式
        mode_label = Label(self._main_pane, text='動圖模式', relief=RIDGE, padx=10)
        mode_label.place(x=10, y=130)

        self._mode = StringVar(value='append')
        ttk.Radiobutton(self._main_pane, text='追加', variable=self._mode, value='append').place(x=90, y=130)
        ttk.Radiobutton(self._main_pane, text='流式', variable=self._mode, value='flow').place(x=145, y=130)
        ttk.Radiobutton(self._main_pane, text='隨機', variable=self._mode, value='random').place(x=200, y=130)

        # 每幀延時
        duration_label = Label(self._main_pane, text='每幀延時', relief=RIDGE, padx=10)
        duration_label.place(x=10, y=170)
        self._duration_scale = Scale(
            self._main_pane, from_=50, to=1000, orient=HORIZONTAL, sliderlength=10
        )
        self._duration_scale.set(250)
        self._duration_scale.place(x=90, y=152)

        Label(self._main_pane, text='(毫秒)').place(x=200, y=170)

        # 整圖幀數
        whole_frames_label = Label(self._main_pane, text='整圖幀數', relief=RIDGE, padx=10)
        whole_frames_label.place(x=10, y=210)

        self._whole_frames_scale = Scale(
            self._main_pane, from_=0, to=20, orient=HORIZONTAL, sliderlength=10
        )
        self._whole_frames_scale.set(10)
        self._whole_frames_scale.place(x=90, y=193)

        Label(self._main_pane, text='(幀)').place(x=200, y=210)

        # 開始轉換
        execute_button = ttk.Button(self._main_pane, text='開始執行', width=23, command=self.__show_gif)
        execute_button.place(x=10, y=250)

    def __select_image(self, event):
        """
        選擇圖片
        """
        image_path = askopenfilename(title='選擇圖片', filetypes=[
            ('PNG', '*.png'), ('JPG', '*.jpg'), ('JPG', '*.jpeg'), ('BMP', '*.bmp'), ('ICO', '*.ico')
        ])
        self._image_path_entry.delete(0, END)
        self._image_path_entry.insert(0, image_path)

    def __block_ranges(self):
        """
        獲取圖片橫向和縱向需要拆分的塊數
        """
        blocks = self._blocks_scale.get()
        if not self._random_block.get():
            n = int(blocks ** 0.5)
            return n, n

        ranges = list()
        for horizontally in range(1, blocks + 1):
            if blocks % horizontally == 0:
                ranges.append((horizontally, blocks // horizontally))

        if ranges:
            return random.choice(ranges)
        else:
            return blocks, 1

    def __generate_materials(self):
        """
        根據原圖生成N張素材圖
        """
        image_path = self._image_path_entry.get()
        if not image_path:
            messagebox.showerror(title='錯誤', message='請選擇圖片')
            return
        self._image_origin = Image.open(image_path)

        # 獲取圖片分辨率
        (width, height) = self._image_origin.size

        # 創建底圖
        self._image_background = Image.new(self._image_origin.mode, self._image_origin.size)
        image_tmp = copy.copy(self._image_background)

        # 獲取橫向和縱向塊數
        horizontally_blocks, vertically_blocks = self.__block_ranges()

        # 計算每塊尺寸
        block_width = width // horizontally_blocks
        block_height = height // vertically_blocks

        width_diff = width - block_width * horizontally_blocks
        height_diff = height - block_height * vertically_blocks

        # GIF模式
        gif_mode = self._mode.get()
        # 生成N幀圖片素材
        materials = list()
        for v_idx, v in enumerate(range(vertically_blocks)):
            for h_idx, h in enumerate(range(horizontally_blocks)):
                _block_width = (h + 1) * block_width
                # 最右一列 寬度+誤差
                if h_idx + 1 == horizontally_blocks:
                    _block_width += width_diff

                _block_height = (v + 1) * block_height
                # 最後一行 高度+誤差
                if v_idx + 1 == vertically_blocks:
                    _block_height += height_diff

                block_box = (h * block_width, v * block_height, _block_width, _block_height)
                block_img = self._image_origin.crop(block_box)
                if gif_mode in ['flow', 'random']:
                    image_tmp = copy.copy(self._image_background)
                image_tmp.paste(block_img, (h * block_width, v * block_height))
                materials.append(copy.copy(image_tmp))

        # mode=random時隨機打亂順序
        if gif_mode == 'random':
            random.shuffle(materials)

        # 整圖幀數
        [materials.append(copy.copy(self._image_origin)) for _ in range(self._whole_frames_scale.get())]

        return materials

    def __show_gif(self):
        """
        展示GIF
        """

        self._materials = self.__generate_materials()
        if not self._materials:
            return

        self._main_pane.place(x=0, y=-1 * self._height)
        self._gif_pane = Frame(self.master, width=self._width, height=self._height)
        self._gif_pane.pack()

        # 設置窗口標題
        self.master.title('預覽GIF')

        label_width = 240
        label = Label(self._gif_pane, width=label_width, height=label_width)
        label.place(x=8, y=5)

        button_save = ttk.Button(self._gif_pane, text='保存', width=9, command=self.__save_gif)
        button_save.place(x=8, y=250)

        button_cancel = ttk.Button(self._gif_pane, text='返回', width=9, command=self.__show_main_pane)
        button_cancel.place(x=138, y=250)

        # 尺寸
        (width, height) = self._image_origin.size
        # 幀速
        duration = self._duration_scale.get()
        # 縮放
        gif_size = (label_width, int(height / width * label_width))

        frames = [ImageTk.PhotoImage(img.resize(gif_size, Image.ANTIALIAS)) for img in self._materials]
        # 幀數
        idx_max = len(frames)

        def show(idx):
            """
            展示圖片
            """
            frame = frames[idx]
            label.configure(image=frame)
            idx = 0 if idx == idx_max else idx + 1
            self._gif_pane.after(duration, show, idx % idx_max)

        show(0)

    def __save_gif(self):
        """
        存儲GIF
        """
        gif_path = asksaveasfilename(title='保存GIF', filetypes=[('GIF', '.gif')])
        if not gif_path:
            return

        gif_path += '' if gif_path.endswith('.gif') or gif_path.endswith('.GIF') else '.gif'
        # 存儲GIF
        Image.new(self._image_origin.mode, self._image_origin.size).save(
            gif_path, save_all=True, loop=True, duration=self._duration_scale.get(), append_images=self._materials
        )

        # 優化GIF
        optimize(gif_path)
        messagebox.showinfo(title='提示', message='保存成功')

        self.__show_main_pane()

    def __show_main_pane(self):
        """
        取消保存
        """
        self.__destroy_frame(self._gif_pane)
        self._main_pane.place(x=0, y=0)


if __name__ == '__main__':
    Img2Gif().mainloop()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章