首页 > 编程语言 >python抽帧及生成高质量的GIF图

python抽帧及生成高质量的GIF图

时间:2024-04-17 12:33:59浏览次数:25  
标签:gif python 调色板 GIF 抽帧 video fps path

python抽帧及生成高质量的GIF图

对视频进行抽帧只需要两个模块即可:

opencv-python (cv2)

opencv-contrib-python

我们都知道视频有分辨率,即视频的宽度与高度,还有视频的帧速率,即每秒有多少帧。

对视频进行抽帧,有两种方式,一种是每秒抽取一帧,另一种是每秒所有帧的抽取。

 

import os
import cv2
import datetime
from pygifsicle import optimize


def extract_frames(video_path, start_time, end_time, is_all=True):
    """
    提取视频帧
    :param video_path: 视频地址
    :param start_time: 开始截取帧的时间
    :param end_time: 结束截取帧的时间
    :param is_all: 是否截图时间段所有帧,如果未来False,则每秒只截取1帧
    :return: ideo_h, video_w, video_fps, out_path
    """

    first_frames = ''
    filename = os.path.basename(video_path)
    file_dir_name = os.path.dirname(video_path)
    file_name = os.path.splitext(filename)[0]
    cap = cv2.VideoCapture(video_path)
    # 视频每秒帧数
    video_fps = int(cap.get(cv2.CAP_PROP_FPS))
    # 视频高度
    video_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    # 视频宽度
    video_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    # 视频时长
    video_duration = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)/video_fps)
   if start_time >= video_duration: start_time = 1 if end_time >= video_duration: end_time = video_duration # 计算起始和结束帧 start_frame = int(start_time * video_fps) end_frame = int(end_time * video_fps) count = 0 proces_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S') out_path = f"{file_dir_name}/{file_name}/{proces_time}/" os.makedirs(out_path)
  # 视频抽帧 while True: ret, frame = cap.read() if not ret: break if count == 0: cv2.imwrite(out_path + f"frame_{count}.png", frame) first_frames = out_path + f"frame_{count}.png" # 如果已经超过结束帧,退出循环 if count >= end_frame: break # 如果当前帧在截取时间内,保存图片 if start_frame <= count: # 如果需要抽取所有帧 if is_all is True: cv2.imwrite(out_path + f"frame_{count}.png", frame) # 如果是每秒只抽取一帧 if is_all is False and (count % video_fps == 0): cv2.imwrite(out_path + f"frame_{count}.png", frame) count += 1 cap.release() return video_h, video_w, video_fps, out_path, first_frames

这里对视频的第一帧和指定的时间段做了抽帧,并返回视频的分辨率以及FPS。

 

接下来就是生成GIF图,图片都有了,合成GIF图很简单了:

用Pillow和imageio模块即可,如果还要压缩GIF的话,再安装pygifsicle这个模块即可:

def pic_to_gif(height, width, fps, pic_path, is_compress=True):
    """
    图片转gif图
    :param height: 生成gif图的高度
    :param width: 生成gif图的宽度
    :param fps: 生成gif图的帧率(每秒图片数)
    :param pic_path: 需要生产gif图的图的地址
    :param is_compress: 是否需要压缩
    :return:
    """
    img_path = []
    for filename in os.listdir(pic_path):
        if filename.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tif', '.tiff')):
            img_path.append(os.path.join(pic_path, filename))
    images = [Image.open(file) for file in img_path]
    cover_img = images[1].filename
    # 设置 GIF 的尺寸和帧率
    size = (width, height)
    gif_file = f"{pic_path}01.gif"
    with imageio.get_writer(gif_file, mode='I', fps=fps) as writer:
        for image in images:
            writer.append_data(image.resize(size))
  
    writer.close()
    # def mimwrite(uri, ims, format=None, **kwargs):
    # with imageio.mimsave(uri=gif_file, ims=images, fps=fps, loop=0) as write:
    # 如果需要压缩GIF图
    if is_compress is True:
        optimize(gif_file, gif_file)
    return gif_file, cover_img

imageio.get_writer 和 imageio.mimsave 方法都是一样的,是不是很简单!但对我来说,才开始。

因为用imageio生成GIF图,质量太不行,而且还比较大。

后来有找了很多生成GIF图的模块,比如moviepy,ImageMagick,都差强人意,值得说的是moviepy:

from moviepy.editor import VideoFileClip

video = VideoFileClip(video_path, verbose=True, has_mask=True)

clip = video.subclip(3, 6)

clip.write_gif("00.gif", program='ffmpeg', fps=4, tempfiles=False, fuzz=0)

"""
def write_gif(self, filename, fps=None, program='imageio',
                  opt='nq', fuzz=1, verbose=True,
                  loop=0, dispose=False, colors=None, tempfiles=False,
                  logger='bar'):

program
          Software to use for the conversion, either 'imageio' (this will use
          the library FreeImage through ImageIO), or 'ImageMagick', or 'ffmpeg'.

opt
          Optimalization to apply. If program='imageio', opt must be either 'wu'
          (Wu) or 'nq' (Neuquant). If program='ImageMagick',
          either 'optimizeplus' or 'OptimizeTransparency'.
"""

moviepy 里的write_gif函数,可以传生成gif图的程序,比如imageio,ffmpeg,ImageMagick

也就是通过moviepy找到了解决方案,这几个程序我都尝试了一遍,当然,ffmpeg,ImageMagick都需要自己安装一下。

发现ffmpeg生成GIF图比imageio要好很多,imageio特别是遇到人脸等啥的,就像打了马赛克一样的。但ffmpeg整体均匀的模糊一样,马赛克不见了,但整体的GIF图质量还是没达到预期。ImageMagick也不行。

找了很多资料,还是在ffmpeg上找到了解决方案:

测试代码:

import subprocess as sp
from moviepy.compat import DEVNULL

globalPalettePicPath = "D:\\thecover_project\\video_to_picture\\video_path\\000.png"
video_path = r"D:\thecover_project\video_to_picture\video_path\video1625112445881755141.mp4"
outFilePath = "D:\\thecover_project\\video_to_picture\\video_path\\1111111.gif"

# video = VideoFileClip(video_path, verbose=True, has_mask=True)
# clip = video.subclip(3, 6)
# clip.write_gif("00.gif", program='ffmpeg', fps=4, tempfiles=False, fuzz=0)
popen_params = {"stdout": DEVNULL, "stderr": DEVNULL, "stdin": sp.PIPE} command = f"ffmpeg -ss 3 -t 2 -i {video_path} -b:v 520k -r 29 -vf fps=29,scale=337:-1:flags=lanczos,palettegen -y {globalPalettePicPath}" proc = sp.Popen(command, **popen_params) proc.communicate() proc.stdin.close() command1 = f"ffmpeg -v error -ss 3 -t 2 -i {video_path} -i {globalPalettePicPath} -r 29 -lavfi fps=29,scale=337:-1:flags=lanczos[x];[x][1:v]paletteuse -y {outFilePath}" # command1 = f"ffmpeg -ss 3 -t 2 -i {video_path} -r 29 -vf fps=29,scale=337:-1 {outFilePath}" proc = sp.Popen(command1, **popen_params) proc.communicate() proc.stdin.close()

 

在生成GIF图前,先对视频进行全局采样,对每一帧的所有颜色制作一个直方图,并且基于这些生成一个调色板,原文详细介绍见这个地址:https://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html

 if video_h >= video_w:
        video_w = 337
        # video_w = 720
    else:
        video_w = 600
        # video_w = 1280
    global_palette_pic_path = f"{out_path}00.png"
    gif_file = f"{out_path}01.gif"
    # 命令通道参数,比如不打印显示日志信息等
    popen_params = {"stdout": DEVNULL, "stderr": DEVNULL, "stdin": sp.PIPE}
    # 定义全局调色板
    command = f"ffmpeg -ss  {start_time} -t {end_time-start_time} -i {video_path} -b:v 520k -r {video_fps} -vf fps={video_fps},scale={video_w}:-1:flags=lanczos,palettegen -y {global_palette_pic_path}"
    proc = sp.Popen(command, **popen_params)
    proc.communicate()
    proc.stdin.close()
    # 通过ffmpeg生成gif图
    command1 = f"ffmpeg -v error -ss  {start_time} -t {end_time-start_time} -i {video_path} -i {global_palette_pic_path} -r {video_fps} -lavfi fps={video_fps},scale={video_w}:-1:flags=lanczos[x];[x][1:v]paletteuse -y {gif_file}"
    proc = sp.Popen(command1, **popen_params)
    proc.communicate()
    proc.stdin.close()
    return first_frames, img_cover, gif_file

这里生成的GIF图,都进行了宽度压缩,竖屏就以宽度337来等比压缩。横屏就以600宽度等比压缩。比较符合网络传播。

 

好了,生成高质量的GIF图,摸索就到这!

 

以下是连接原文的转载:

使用 FFmpeg 的高品质 GIF

大约两年前,我尝试改进 FFmpeg 中对 GIF 编码的支持,使其至少像样。这尤其导致了 GIF 编码器中添加透明机制。虽然根据您的来源,这并不总是最佳的,但在最常见的情况下是这样。尽管如此,这只是为了防止编码器受到过多的羞辱。

但最近在Stupeflix ,我们需要一种为Legend 应用程序生成高质量 GIF 的方法,所以我决定再次致力于此。

本博文中介绍的所有功能都在FFmpeg 2.6中可用,并将在 Legend 应用程序的下一版本中使用(可能在 3 月 26 日左右)。

TL;DR:转到“用法”部分查看如何使用它。

初步改进(2013)

我们来观察一下2013年引入的透明机制在GIF编码器中的效果:

% ffmpeg -v warning -ss 45 -t 2 -i big_buck_bunny_1080p_h264.mov -vf scale=300:-1 -gifflags -transdiff -y bbb-notrans.gif
% ffmpeg -v warning -ss 45 -t 2 -i big_buck_bunny_1080p_h264.mov -vf scale=300:-1 -gifflags +transdiff -y bbb-trans.gif
% ls -l bbb-*.gif
-rw-r--r-- 1 ux ux 1.1M Mar 15 22:50 bbb-notrans.gif
-rw-r--r-- 1 ux ux 369K Mar 15 22:50 bbb-trans.gif

居中

默认情况下启用此选项,因此您只需在图像有大量运动或颜色变化时才需要禁用它。

另一种实现的压缩机制是裁剪,这基本上是一种只允许重绘 GIF 的子矩形并保持周围不变的方法。对于电影来说,它很少有用。我稍后会再讨论这个问题。

但无论如何,从那时起,我在这方面就没有任何进展。虽然在上图中可能不太明显,但在质量方面存在不少缺陷。

256 色限制

您可能知道,GIF 的调色板仅限于 256 种颜色。默认情况下,FFmpeg 仅使用通用调色板,尝试覆盖整个颜色空间以支持最多种内容:

居中

有序和误差扩散抖动

为了避免这个问题,使用了抖动。在上面的 Big Buck Bunny GIF 中,应用了有序拜耳抖动。它很容易通过其8x8交叉阴影线图案来识别。虽然它不是最漂亮的,但它有很多好处,例如可预测快速,并且实际上可以防止条带效应和类似的视觉故障。

您会发现大多数其他抖动方法都是基于错误的。其原理是,单一颜色错误(调色板中选取的颜色与预期颜色之间的差异)将遍布整个图像,导致帧之间出现“蜂拥效应”,即使在帧之间源完全相同的区域中也是如此。虽然这通常可以提供更好的质量,但它完全破坏了 GIF 的压缩:

% ffmpeg -v warning -ss 45 -t 2 -i big_buck_bunny_1080p_h264.mov -vf scale=300:-1:sws_dither=ed -y bbb-error-diffusal.gif
% ls -l bbb-error-diffusal.gif
-rw-r--r-- 1 ux ux 1.3M Mar 15 23:10 bbb-error-diffusal.gif

更好的调色板

提高 GIF 质量的第一步是定义更好的调色板。 GIF格式存储了一个全局调色板,您可以为一张图片(或子图片;每一帧叠加在前一帧上,但可以以更小的尺寸以特定偏移量叠加)重新定义调色板。每帧调色板仅取代一帧的全局调色板。一旦您停止定义调色板,它就会退回到全局调色板。这意味着您无法像您想做的那样为一系列帧定义调色板(通常在每个场景更改时定义一个新调色板)。

换句话说,您必须遵循一个全局调色板或每帧一个调色板的模型。

每帧一个调色板(未实现)

我最初是从实现每帧调色板的计算开始的,但这有以下缺点:

  • 开销:256 色调色板为 768B,它不是 LZW算法机制的一部分,因此不会被压缩。由于必须按每一帧存储它,这意味着 25 FPS 素材的开销为 150 kbits/sec。但它大多可以忽略不计。
  • 我最初的测试是由于这些调色板的变化而产生亮度闪烁效果,这根本不漂亮。

这是我没有遵循该路径并决定计算全局调色板的两个原因。现在我回想起来,可能需要重试该方法,因为颜色量化的状态比我最初的测试更好。

还可以在帧范围的每个帧处存储相同的调色板(通常在场景变化时,如前面提到的)。或者更好,仅针对发生变化的子矩形。

所有这些都留给读者作为练习,欢迎补丁。如果您对此感兴趣,请随时与我联系。

一个全局调色板(已实现)

拥有一个全局调色板意味着 2 遍机制(除非您愿意将所有视频帧存储在内存中)。

第一遍是计算整个演示文稿的调色板。这就是新的调色板生成过滤器发挥作用的地方。该滤镜对每帧的所有颜色制作直方图,并从中生成调色板。

关于技术方面的一些琐事:过滤器正在实现 Paul Heckbert 的帧缓冲区显示彩色图像量化 (1982)论文中的算法的变体。以下是我记得所做的差异(或论文中未定义行为的特殊性):

  • 它使用全分辨率颜色直方图。因此,该过滤器没有像论文中建议的那样使用下采样 RGB 5:5:5 直方图作为关键,而是使用哈希表来存储 1600 万种可能的 RGB 8:8:8 颜色。
  • 盒子的分割仍然是在中点处进行,但是根据盒子中颜色的方差来选择要分割的盒子(颜色方差较大的盒子将被优先选择为截止) )。
  • 据我所知,这在论文中没有定义,但框中颜色的平均值是根据颜色的重要性来完成的。
  • 当沿轴(红、绿或蓝)分割盒子时,在相等的情况下,绿色优先于红色,而红色又优先于蓝色。

所以无论如何,这个过滤器会进行颜色量化,并生成调色板(通常保存到文件中PNG)。

它通常看起来像这样(放大):

居中

颜色映射和抖动

然后,第二遍由调色板使用过滤器处理,正如您从名称中猜测的那样,它将使用该调色板生成最终的颜色量化流。它的任务是在生成的调色板中找到最合适的颜色来表示输入颜色。您也可以在此处决定使用哪种抖动方法。

再说一些技术方面的琐事:

  • 虽然原始论文仅提出了一种抖动方法,但滤波器实现了其中 5 种。
  • 就像 一样palettegen,颜色分辨率(将 24 位输入颜色映射到调色板条目)是在不破坏输入的情况下完成的。它是通过Kd Tree的迭代实现(显然 K=3,每个 RGB 分量一个维度)和缓存系统来实现的。

使用这两个过滤器将允许您像这样编码 GIF(单个全局调色板,无抖动):

居中

用法

使用相同的参数手动运行 2 个通道可能会有点烦人,需要调整每个通道的参数,因此我建议编写一个简单的脚本,例如:

#!/bin/sh

palette="/tmp/palette.png"
filters="fps=15,scale=320:-1:flags=lanczos"

ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2

...可以这样使用:

% ./gifenc.sh video.mkv anim.gif

filters变量包含在这里:

  • 每秒帧数的调整(减少到 15 可能会导致视觉上的抖动,但会使最终的 GIF 更小)
  • 使用lanczos缩放器而不是默认的缩放器(bilinear当前)。建议您使用lanczos或重新缩放,bicubic因为它们远远优于bilinear。如果不这样做,您的输入很可能会更加模糊。

仅提取样本

您不太可能对完整的电影进行编码,因此您可能会想使用-ss-t(或类似)选项来选择片段。如果您这样做,请务必将两者都作为输入选项(在 之前-i)。例如:

#!/bin/sh

start_time=12:23
duration=35
palette="/tmp/palette.png"
filters="fps=15,scale=320:-1:flags=lanczos"

ffmpeg -v warning -ss $start_time -t $duration -i $1 -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -ss $start_time -t $duration -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2

如果不这样做,至少在第一遍中会导致问题,其中输出永远不会超过一帧(调色板),因此不会达到您想要的效果。

一种替代方法是使用流复制来预提取要编码的样本,例如:

% ffmpeg -ss 12:23 -t 35 -i full.mkv -c:v copy -map 0:v -y video.mkv

如果流副本不够准确,您可以添加修剪 过滤器。例如:

filters="trim=start_frame=12:end_frame=431,fps=15,scale=320:-1:flags=lanczos"

充分利用调色板一代

现在我们可以开始看看有趣的部分了。在palettegen过滤器中,您想要进行的主要且可能唯一的调整是选项stats_mode 。

此选项基本上允许您指定您是对整个/整体视频更感兴趣,还是仅对正在移动的内容更感兴趣。如果使用stats_mode=full (默认),所有像素都将成为颜色统计的一部分。如果使用 stats_mode=diff,则仅考虑与前一帧不同的像素。

注意:要向过滤器添加选项,请像这样使用它: thefilter=opt1=value1:opt2=value2

以下是一个示例来说明它如何影响最终输出:

居中 居中

第一个 GIF 正在使用stats_mode=full(默认)。整个演示文稿的背景不会改变,因此天空的颜色受到了很多关注。另一方面,移动文本最终的颜色子集非常有限。结果,文本的淡出受到了影响:

居中

另一方面,第二个 GIF 使用stats_mode=diff,这有利于移动的内容。事实上,文本的淡出要好得多,但代价是天空中出现抖动故障:

居中

充分利用颜色映射

paletteuse过滤器有更多的选项可供使用。最明显的一个是抖动(dither选项)。唯一可用的可预测抖动是bayer,所有其他抖动都是基于误差扩散的。

如果您确实想使用bayer(因为您有高速或尺寸问题),您可以使用bayer_scale降低或增加其剖面线图案的选项。

当然,您也可以使用 来完全禁用抖动 dither=none

关于误差扩散抖动,您需要使用 floyd_steinbergsierra2sierra2_4a。有关这些的更多详细信息,我将您重定向到DHALF.TXT

对于懒人来说,floyd_steinberg是最流行的之一,并且sierra2_4a是(并且是默认的)的快速/较小版本sierra2,通过 3 个像素而不是 7 个像素进行扩散。heckbert是我之前提到的论文中记录的一个,并且只是包含为一个参考(你可能不需要它)。

以下是不同抖动模式的小预览:

原始(31.82K): 居中

dither=bayer:bayer_scale=1(132.80K): 居中

dither=bayer:bayer_scale=2(118.80K): 居中

dither=bayer:bayer_scale=3(103.11K): 居中

dither=floyd_steinberg(101.78K): 居中

dither=sierra2(89.98K): 居中

dither=sierra2_4a(109.60K): 居中

dither=none(73.10K): 居中

最后,在尝试了抖动之后,您可能有兴趣了解该选项diff_mode。引用文档:

只有变化的矩形才会被重新处理。这类似于 GIF 裁剪/偏移压缩机制。如果仅图像的一部分发生变化,则此选项对于提高速度很有用,并且具有一些用例,例如将误差扩散抖动的范围限制为限制移动场景的矩形(如果场景不发生变化,则它会导致更具确定性的输出)变化不大,因此移动噪音更少,GIF 压缩效果更好)。

或者换句话说:如果您想在图像上使用误差扩散抖动作为背景,即使它是静态的,请启用此选项来限制误差在整个图片上的传播。这是一个相关的典型案例:

居中

请注意,只有当顶部和底部文本同时移动时(即,在此处的最后一帧中),猴子脸上的抖动才会发生变化。

 

标签:gif,python,调色板,GIF,抽帧,video,fps,path
From: https://www.cnblogs.com/drewgg/p/18140299

相关文章

  • python封装工具类之excel读写
    在python自动化过程中,如果测试用例数据在excel中,就需要去对excel进行读写操作这个测试类主要是读取excel中的测试用例,然后再将测试结果回写到excel中。excel表格格式示例(cases.xlsx):case_idcase_namemethodurldataexpectedactualresult1用户正常登录post/log......
  • Python 数据结构和算法实用指南(三)
    原文:zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d译者:飞龙协议:CCBY-NC-SA4.0第七章:哈希和符号表我们之前已经看过数组和列表,其中项目按顺序存储并通过索引号访问。索引号对计算机来说很有效。它们是整数,因此快速且易于操作。但是,它们并不总是对我们很有效......
  • [Python]OS模块应用
    OS提供许多和操作系统交互的功能,允许访问文件,目录,进程,环境变量等。导入模块,importos获取当前工作目录,os.getcwd()current_dir=os.getcwd()print("当前工作目录:",current_dir)>>>当前工作目录:C:\Users\wuyucun创建目录,os.mkdir()current_dir=os.getcwd()......
  • Python 数据结构和算法实用指南(一)
    原文:zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d译者:飞龙协议:CCBY-NC-SA4.0前言数据结构和算法是信息技术和计算机科学工程学习中最重要的核心学科之一。本书旨在提供数据结构和算法的深入知识,以及编程实现经验。它专为初学者和中级水平的研究Python编......
  • Python 数据结构和算法实用指南(二)
    原文:zh.annas-archive.org/md5/66ae3d5970b9b38c5ad770b42fec806d译者:飞龙协议:CCBY-NC-SA4.0第四章:列表和指针结构我们已经在Python中讨论了列表,它们方便而强大。通常情况下,我们使用Python内置的列表实现来存储任何数据。然而,在本章中,我们将了解列表的工作原理,并将研......
  • python生成exe
    pyinstaller:PyInstaller是一个用于打包Python应用程序的工具,它可以将Python脚本打包成可执行文件,无需依赖外部Python解释器。你可以使用PyInstaller来将整个文件夹中的多个Python文件打包成一个可执行文件。你可以通过以下命令安装PyInstaller:pipinstallpyinstal......
  • Python——cProfile(程序分析)
    程序分析可以系统性地分析程序的运行速度、内存使用情况等。cProfile是Python的分析器,用于测量程序的运行时间和程序内各个函数调用消耗的时间。importcProfiledefadd():total=0foriinrange(1,10000001):total+=icProfile.run('add()')'''......
  • python-pptx 学习
      1frompptximportPresentation2frompptx.utilimportInches3frompptx.chart.dataimportChartData4frompptx.enum.chartimportXL_TICK_MARK5frompptx.utilimportPt6frompptx.dml.colorimportRGBColor7frompptx.enum.chartim......
  • 2024.4.16python基础学习
    基本数据类型numberintmoney=6600floatdiscount=1.2boolenisok=trueisok=falsestrings='sssss's="ssssss"ps:单引号与双引号成对出现,不可以混合使用可以单引号嵌套双引号,互相嵌套list(列表)my_list=['足球','篮球']tuple(元组)my_tuple=(12,123,1234)dict(字典)......
  • 【python】如何优雅的终止while循环
    1.背景需求python需要与外界或终端交互时,常常需要使用while循环一直跑。如果需要终止程序,一般使用Ctrl+c,此时终端会打印一大堆backtrace,并且无法保留当前运行的状态,非常不优雅。使用KeyboardInterrupt异常捕捉,可以实现优雅的终止while循环。 2.实现方法try:while(Tr......