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

生成高质量的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宽度等比压缩。比较符合网络传播。

结果如下:00.png存储采样图,01.gif为生成的gif图,第0帧,以及第5秒的第一帧。

 

好了,生成高质量的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 压缩效果更好)。

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

居中

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

 

posted @ 2024-04-17 12:24  drewgg  阅读(168)  评论(0编辑  收藏  举报