当前位置: 首页 > article >正文

Python-基于PyQt5,Pillow,pathilb,imageio,moviepy,sys的GIF(动图)制作工具

前言:在抖音,快手等社交平台上,我们常常见到各种各样的GIF动画。在各大评论区里面,GIF图片以其短小精悍、生动有趣的特点,被广泛用于分享各种有趣的场景、搞笑的瞬间、精彩的动作等,能够快速吸引我们的注意力,增强内容的传播性和互动性。生活中,我们将各种有趣的人物表情、动作、台词等制作成GIF表情包,既可以更生动地表达我们此时的情感和态度,也让聊天的过程拥有了更多的趣味性和幽默感。当然,GIF动画不止在娱乐领域里应用广泛,在计算机的网页设计中很多时候也会使用GIF动画可以为页面增添动态效果,使页面更加生动活泼,吸引用户的注意力。例如,可以在网页的标题、导航栏、按钮等元素中添加GIF动画,提升页面的视觉效果和用户体验等。总而言之,GIF动画在我们的日常生活中扮演着重要的角色,我们有必要了解GIF动画的制作方法及相关制作工具。话不多说,我们今天就来学习一下如何利用Python来制作一款GIF动画工具。

编程思路:本次编程我们将会调用到Python中的众多库:包括诸如PyQt5,pillow,moviepy等的第三方库和sys,pathlib等的标准库。PyQt5被用于创建一个图形用户界面 (GUI) 应用程序(具体为代码中的GifMakerGUI类)。我们将创建窗口和布局(这里包括GUI窗口的大小,位置等),创建GUI中的相关组件(如按钮,标签,菜单等),处理事件和信号(主要负责将用户触发的事件与GUI控件联系起来),应用程序的启动和运行等。Pillow是Python中很重要的一个图片处理库,利用它我们可以对图片进行图像操作(包括图片的加载,操作,保存等),图像转换(包括图像颜色表示模式的转换(如RGB转HSV),以及图像尺寸大小的设置),图像序列处理(保存图像序列为GIF或其他格式),图像合成等操作。与pillow不同,moviepy是一个视频文件处理库(具体来说,它可以进行视频剪辑(打开,截取视频文件,也能进行音频处理(合成音频剪辑,视频音频合并,音频文件保存等))。imageio库比较简单,它主要被用于处理图像序列(简单来说就是将一系列图像保存为动画文件,如本次的GIF)。

第一步:导入库

标准库:sys,pathlib。

第三方库:PyQt5,pillow,imageio,moviepy。

#导入库
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from moviepy import VideoFileClip, CompositeAudioClip
import imageio
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton, QFileDialog,
                             QProgressBar, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal

第二步:创建功能类

两个:

GifCreator类,在后台线程中创建GIF或MP4文件(主要取决于用户是否选择音频文件(不选则生成gif动图,选则生成MP4文件))。

GifMakerGUI类,用于创建一个图形用户界面(GUI)应用程序,允许我们选择源文件(视频或图片)、添加音频文件、设置输出路径和参数,并启动GIF或MP4文件(同上)的生成过程。

#创建gif生成类
class GifCreator(QThread):
    progress_updated = pyqtSignal(int)
    finished = pyqtSignal(str)
    error_occurred = pyqtSignal(str)

    #初始化函数
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.font = None
        if config['text']:
            try:
                self.font = ImageFont.truetype("arial.ttf", 24)
            except:
                self.font = ImageFont.load_default()

    def run(self):
        try:
            if self.config['audio_path'] and not self.config['output'].endswith('.mp4'):
                self.error_occurred.emit("音频仅支持MP4输出格式")
                return

            if self.config['source_type'] == 'video':
                self._create_from_video()
            else:
                self._create_from_images()

            if self.config['audio_path']:
                self._add_audio()

            self.finished.emit(self.config['output'])
        except Exception as e:
            self.error_occurred.emit(str(e))

    def _process_frame(self, frame, index):
        img = Image.fromarray(frame).resize(self.config['size'])
        if self.config['text']:
            draw = ImageDraw.Draw(img)
            draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))
        self.progress_updated.emit(int((index + 1) / self.total_frames * 100))
        return img

    def _create_from_video(self):
        with VideoFileClip(str(self.config['sources'][0])) as clip:
            if self.config['duration']:
                clip = clip.subclip(0, self.config['duration'])
            self.total_frames = int(clip.duration * self.config['fps'])
            frames = []
            for i, frame in enumerate(clip.iter_frames(fps=self.config['fps'])):
                frames.append(self._process_frame(frame, i))

        frames[0].save(
            self.config['output'],
            save_all=True,
            append_images=frames[1:],
            optimize=True,
            duration=1000 // self.config['fps'],
            loop=0
        )

    def _create_from_images(self):
        images = []
        self.total_frames = len(self.config['sources'])
        for i, img_path in enumerate(self.config['sources']):
            with Image.open(img_path) as img:
                img = img.convert('RGBA').resize(self.config['size'])
                if self.config['text']:
                    draw = ImageDraw.Draw(img)
                    draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))
                images.append(img)
            self.progress_updated.emit(int((i + 1) / self.total_frames * 100))

        imageio.mimsave(
            self.config['output'],
            [img.convert('P', palette=Image.ADAPTIVE) for img in images],
            fps=self.config['fps'],
            palettesize=256
        )

    def _add_audio(self):
        video_clip = VideoFileClip(self.config['output'])
        audio_clip = CompositeAudioClip([VideoFileClip(self.config['audio_path']).audio])
        final_clip = video_clip.set_audio(audio_clip)
        final_clip.write_videofile(self.config['output'], codec='libx264')

#创建主程窗口类
class GifMakerGUI(QWidget):
    #初始化函数
    def __init__(self):
        super().__init__()
        self.initUI()
        self.worker = None

    #初始化用户界面
    def initUI(self):
        self.setWindowTitle('GIF制作工具(初级)')
        self.setGeometry(300, 300, 600, 400)

        layout = QVBoxLayout()

        # 源文件选择
        self.source_btn = QPushButton('选择源文件(视频/图片)')
        self.source_btn.clicked.connect(self.select_source)
        self.source_label = QLabel('未选择文件')
        layout.addWidget(self.source_btn)
        layout.addWidget(self.source_label)

        # 音频文件选择
        self.audio_btn = QPushButton('添加音频文件')
        self.audio_btn.clicked.connect(self.select_audio)
        self.audio_label = QLabel('未选择音频文件')
        layout.addWidget(self.audio_btn)
        layout.addWidget(self.audio_label)

        # 输出设置
        output_layout = QHBoxLayout()
        self.output_btn = QPushButton('选择输出路径')
        self.output_btn.clicked.connect(self.select_output)
        self.output_entry = QLineEdit()
        output_layout.addWidget(self.output_btn)
        output_layout.addWidget(self.output_entry)
        layout.addLayout(output_layout)

        # 参数设置
        params_layout = QHBoxLayout()
        self.fps_entry = QLineEdit('10')
        self.size_w_entry = QLineEdit('640')
        self.size_h_entry = QLineEdit('480')
        self.duration_entry = QLineEdit()
        self.text_entry = QLineEdit()
        params_layout.addWidget(QLabel('FPS:'))
        params_layout.addWidget(self.fps_entry)
        params_layout.addWidget(QLabel('宽:'))
        params_layout.addWidget(self.size_w_entry)
        params_layout.addWidget(QLabel('高:'))
        params_layout.addWidget(self.size_h_entry)
        layout.addLayout(params_layout)

        text_layout = QHBoxLayout()
        text_layout.addWidget(QLabel('文字水印:'))
        text_layout.addWidget(self.text_entry)
        layout.addLayout(text_layout)

        # 进度条
        self.progress = QProgressBar()
        layout.addWidget(self.progress)

        # 操作按钮
        self.start_btn = QPushButton('开始生成')
        self.start_btn.clicked.connect(self.start_process)
        layout.addWidget(self.start_btn)

        self.setLayout(layout)

    def select_source(self):
        files, _ = QFileDialog.getOpenFileNames(
            self, '选择需处理的文件', '',
            '视频文件 (*.mp4 *.mov *.avi);;图片文件 (*.png *.jpg *.jpeg)'
        )
        if files:
            self.source_label.setText(f'已选择 {len(files)} 个文件')
            self.source_files = [Path(f) for f in files]

    def select_audio(self):
        file, _ = QFileDialog.getOpenFileName(
            self, '选择音频文件', '', '音频文件 (*.mp3 *.wav)'
        )
        if file:
            self.audio_label.setText(Path(file).name)
            self.audio_file = file

    def select_output(self):
        file, _ = QFileDialog.getSaveFileName(
            self, '保存输出文件', '',
            'GIF文件 (*.gif);;MP4文件 (*.mp4)'
        )
        if file:
            self.output_entry.setText(file)

    def validate_input(self):
        required = [
            (self.source_files, '请选择源文件'),
            (self.output_entry.text(), '请设置输出路径'),
            (self.fps_entry.text().isdigit(), 'FPS必须为数字'),
            (self.size_w_entry.text().isdigit(), '宽度必须为数字'),
            (self.size_h_entry.text().isdigit(), '高度必须为数字')
        ]
        for condition, message in required:
            if not condition:
                QMessageBox.warning(self, '输入错误', message)
                return False
        return True

    def start_process(self):
        if not self.validate_input():
            return

        config = {
            'sources': self.source_files,
            'output': self.output_entry.text(),
            'fps': int(self.fps_entry.text()),
            'size': (int(self.size_w_entry.text()), int(self.size_h_entry.text())),
            'duration': None,  # 可添加持续时间设置
            'text': self.text_entry.text(),
            'audio_path': getattr(self, 'audio_file', None),
            'source_type': 'video' if self.source_files[0].suffix.lower() in ['.mp4', '.mov', '.avi'] else 'image'
        }

        self.worker = GifCreator(config)
        self.worker.progress_updated.connect(self.update_progress)
        self.worker.finished.connect(self.process_finished)
        self.worker.error_occurred.connect(self.show_error)
        self.start_btn.setEnabled(False)
        self.worker.start()

    def update_progress(self, value):
        self.progress.setValue(value)

    def process_finished(self, output_path):
        self.start_btn.setEnabled(True)
        QMessageBox.information(self, '完成', f'文件已生成:{output_path}')

    def show_error(self, message):
        self.start_btn.setEnabled(True)
        QMessageBox.critical(self, '错误', message)

第三步:创建驱动单元

我们需要创建一个单独的单元来驱动整个程序的正常运行,这就是驱动单元。

#驱动程序单元
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = GifMakerGUI()
    ex.show()
    sys.exit(app.exec_())

第四步:完整代码展示

#导入库
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from moviepy import VideoFileClip, CompositeAudioClip
import imageio
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton, QFileDialog,
                             QProgressBar, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal

#创建gif生成类
class GifCreator(QThread):
    progress_updated = pyqtSignal(int)
    finished = pyqtSignal(str)
    error_occurred = pyqtSignal(str)

    #初始化函数
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.font = None
        if config['text']:
            try:
                self.font = ImageFont.truetype("arial.ttf", 24)
            except:
                self.font = ImageFont.load_default()

    def run(self):
        try:
            if self.config['audio_path'] and not self.config['output'].endswith('.mp4'):
                self.error_occurred.emit("音频仅支持MP4输出格式")
                return

            if self.config['source_type'] == 'video':
                self._create_from_video()
            else:
                self._create_from_images()

            if self.config['audio_path']:
                self._add_audio()

            self.finished.emit(self.config['output'])
        except Exception as e:
            self.error_occurred.emit(str(e))

    def _process_frame(self, frame, index):
        img = Image.fromarray(frame).resize(self.config['size'])
        if self.config['text']:
            draw = ImageDraw.Draw(img)
            draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))
        self.progress_updated.emit(int((index + 1) / self.total_frames * 100))
        return img

    def _create_from_video(self):
        with VideoFileClip(str(self.config['sources'][0])) as clip:
            if self.config['duration']:
                clip = clip.subclip(0, self.config['duration'])
            self.total_frames = int(clip.duration * self.config['fps'])
            frames = []
            for i, frame in enumerate(clip.iter_frames(fps=self.config['fps'])):
                frames.append(self._process_frame(frame, i))

        frames[0].save(
            self.config['output'],
            save_all=True,
            append_images=frames[1:],
            optimize=True,
            duration=1000 // self.config['fps'],
            loop=0
        )

    def _create_from_images(self):
        images = []
        self.total_frames = len(self.config['sources'])
        for i, img_path in enumerate(self.config['sources']):
            with Image.open(img_path) as img:
                img = img.convert('RGBA').resize(self.config['size'])
                if self.config['text']:
                    draw = ImageDraw.Draw(img)
                    draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))
                images.append(img)
            self.progress_updated.emit(int((i + 1) / self.total_frames * 100))

        imageio.mimsave(
            self.config['output'],
            [img.convert('P', palette=Image.ADAPTIVE) for img in images],
            fps=self.config['fps'],
            palettesize=256
        )

    def _add_audio(self):
        video_clip = VideoFileClip(self.config['output'])
        audio_clip = CompositeAudioClip([VideoFileClip(self.config['audio_path']).audio])
        final_clip = video_clip.set_audio(audio_clip)
        final_clip.write_videofile(self.config['output'], codec='libx264')

#创建主程窗口类
class GifMakerGUI(QWidget):
    #初始化函数
    def __init__(self):
        super().__init__()
        self.initUI()
        self.worker = None

    #初始化用户界面
    def initUI(self):
        self.setWindowTitle('GIF制作工具(初级)')
        self.setGeometry(300, 300, 600, 400)

        layout = QVBoxLayout()

        # 源文件选择
        self.source_btn = QPushButton('选择源文件(视频/图片)')
        self.source_btn.clicked.connect(self.select_source)
        self.source_label = QLabel('未选择文件')
        layout.addWidget(self.source_btn)
        layout.addWidget(self.source_label)

        # 音频文件选择
        self.audio_btn = QPushButton('添加音频文件')
        self.audio_btn.clicked.connect(self.select_audio)
        self.audio_label = QLabel('未选择音频文件')
        layout.addWidget(self.audio_btn)
        layout.addWidget(self.audio_label)

        # 输出设置
        output_layout = QHBoxLayout()
        self.output_btn = QPushButton('选择输出路径')
        self.output_btn.clicked.connect(self.select_output)
        self.output_entry = QLineEdit()
        output_layout.addWidget(self.output_btn)
        output_layout.addWidget(self.output_entry)
        layout.addLayout(output_layout)

        # 参数设置
        params_layout = QHBoxLayout()
        self.fps_entry = QLineEdit('10')
        self.size_w_entry = QLineEdit('640')
        self.size_h_entry = QLineEdit('480')
        self.duration_entry = QLineEdit()
        self.text_entry = QLineEdit()
        params_layout.addWidget(QLabel('FPS:'))
        params_layout.addWidget(self.fps_entry)
        params_layout.addWidget(QLabel('宽:'))
        params_layout.addWidget(self.size_w_entry)
        params_layout.addWidget(QLabel('高:'))
        params_layout.addWidget(self.size_h_entry)
        layout.addLayout(params_layout)

        text_layout = QHBoxLayout()
        text_layout.addWidget(QLabel('文字水印:'))
        text_layout.addWidget(self.text_entry)
        layout.addLayout(text_layout)

        # 进度条
        self.progress = QProgressBar()
        layout.addWidget(self.progress)

        # 操作按钮
        self.start_btn = QPushButton('开始生成')
        self.start_btn.clicked.connect(self.start_process)
        layout.addWidget(self.start_btn)

        self.setLayout(layout)

    def select_source(self):
        files, _ = QFileDialog.getOpenFileNames(
            self, '选择需处理的文件', '',
            '视频文件 (*.mp4 *.mov *.avi);;图片文件 (*.png *.jpg *.jpeg)'
        )
        if files:
            self.source_label.setText(f'已选择 {len(files)} 个文件')
            self.source_files = [Path(f) for f in files]

    def select_audio(self):
        file, _ = QFileDialog.getOpenFileName(
            self, '选择音频文件', '', '音频文件 (*.mp3 *.wav)'
        )
        if file:
            self.audio_label.setText(Path(file).name)
            self.audio_file = file

    def select_output(self):
        file, _ = QFileDialog.getSaveFileName(
            self, '保存输出文件', '',
            'GIF文件 (*.gif);;MP4文件 (*.mp4)'
        )
        if file:
            self.output_entry.setText(file)

    def validate_input(self):
        required = [
            (self.source_files, '请选择源文件'),
            (self.output_entry.text(), '请设置输出路径'),
            (self.fps_entry.text().isdigit(), 'FPS必须为数字'),
            (self.size_w_entry.text().isdigit(), '宽度必须为数字'),
            (self.size_h_entry.text().isdigit(), '高度必须为数字')
        ]
        for condition, message in required:
            if not condition:
                QMessageBox.warning(self, '输入错误', message)
                return False
        return True

    def start_process(self):
        if not self.validate_input():
            return

        config = {
            'sources': self.source_files,
            'output': self.output_entry.text(),
            'fps': int(self.fps_entry.text()),
            'size': (int(self.size_w_entry.text()), int(self.size_h_entry.text())),
            'duration': None,  # 可添加持续时间设置
            'text': self.text_entry.text(),
            'audio_path': getattr(self, 'audio_file', None),
            'source_type': 'video' if self.source_files[0].suffix.lower() in ['.mp4', '.mov', '.avi'] else 'image'
        }

        self.worker = GifCreator(config)
        self.worker.progress_updated.connect(self.update_progress)
        self.worker.finished.connect(self.process_finished)
        self.worker.error_occurred.connect(self.show_error)
        self.start_btn.setEnabled(False)
        self.worker.start()

    def update_progress(self, value):
        self.progress.setValue(value)

    def process_finished(self, output_path):
        self.start_btn.setEnabled(True)
        QMessageBox.information(self, '完成', f'文件已生成:{output_path}')

    def show_error(self, message):
        self.start_btn.setEnabled(True)
        QMessageBox.critical(self, '错误', message)

#驱动程序单元
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = GifMakerGUI()
    ex.show()
    sys.exit(app.exec_())

第五步:运行效果展示

第六步:操作指南

运行程序,待程序初始化完成弹出窗口后,点击"选择源文件(视频/图片)",在系统中选择你想要处理的视频/图片("添加音频文件"这一步可以省略,因为本次我们学习的是GIF制作,不需要这一步)。接着我们点击"选择输出路径",选择最终生成的GIF存放的位置。接着自行设置FPS(帧数),GIF高,宽以及"文字水印"的内容等。最后点击"开始生成"按钮,窗口会出现进对条提示处理进度,当进度条满100%后,需再等待一段时间(此时程序正在将处理好的文件存放在指定位置)。当窗口弹出小窗口提示"文件已生成",点击小窗口中的"OK"按钮。返回你设置的"选择输出路径"的存放路径,就可以看到生成的GIF动画。

第七步:注意事项

- FPS值越大,文件处理时间越长,请谨慎设置;GIF的高,宽同理。(已设置默认值)

- 水印暂不支持中文字体,后面会改进。

- 水印颜色默认为红色,位置为视频/图片左上方。

- 文件的大小同样会影响程序的处理时间。

后面我会对以上问题进行优化/处理,并添加更多新奇功能,敬请期待!


http://www.kler.cn/a/533900.html

相关文章:

  • Meta推动虚拟现实:Facebook如何进入元宇宙时代
  • Got socket exception during request. It might be caused by SSL misconfiguration
  • 离散时间傅里叶变换(DTFT)公式详解:周期性与连续性剖析
  • simpleQtLogger日志库的使用
  • Vue Dom截图插件,截图转Base64 html2canvas
  • 【工欲善其事】利用 DeepSeek 实现复杂 Git 操作:从原项目剥离出子版本树并同步到新的代码库中
  • 探索 paraphrase-MiniLM-L6-v2 模型在自然语言处理中的应用
  • 【深度学习入门_机器学习理论】决策树(Decision Tree)
  • C# 中记录(Record)详解
  • JS-对象-BOM
  • 基于SpringBoot+vue高效旅游管理系统
  • 基础相对薄弱怎么考研
  • Clojure语言的软件工程
  • 鸿蒙5.0进阶开发:UI开发-富文本(RichEditor)
  • dl学习笔记(8):fashion-mnist
  • maven本地打包依赖无法引用
  • 基于微信小程序的培训机构客户管理系统设计与实现(LW+源码+讲解)
  • 动态规划——斐波那契数列模型问题
  • Java 进阶 01 —— 5 分钟回顾一下 Java 基础知识
  • 【华为OD-E卷 - 107 连续出牌数量 100分(python、java、c++、js、c)】
  • 使用 Deepseek AI 制作视频的完整教程
  • 神经网络常见激活函数 2-tanh函数(双曲正切)
  • 63.网页请求与按钮禁用 C#例子 WPF例子
  • 低代码系统-产品架构案例介绍、蓝凌(十三)
  • 4.PPT:日月潭景点介绍【18】
  • Python爬虫实战:一键采集电商数据,掌握市场动态!