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_steinberg
、sierra2
和sierra2_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 压缩效果更好)。
或者换句话说:如果您想在图像上使用误差扩散抖动作为背景,即使它是静态的,请启用此选项来限制误差在整个图片上的传播。这是一个相关的典型案例:
请注意,只有当顶部和底部文本同时移动时(即,在此处的最后一帧中),猴子脸上的抖动才会发生变化。
本文来自博客园,作者:drewgg,转载请注明原文链接:https://www.cnblogs.com/drewgg/p/18140299