7. 处理视频文件#

有时需要从标准视频文件(如 .avi 和 .mov 文件)中读取一系列图像。

在科学背景下,通常最好避免这些格式,而选择简单的图像目录或多维TIF。视频格式更难以逐段读取,通常不支持随机帧访问或研究导向的元数据,并且在未仔细配置的情况下使用有损压缩。但视频文件被广泛使用,并且易于共享,因此在必要时能够读写它们是很方便的。

用于读取视频文件的工具在安装和使用的便捷性、磁盘和内存使用情况以及跨平台兼容性方面各不相同。这是一本实用指南。

7.1. 一个解决方案:将视频转换为图像序列#

对于一次性解决方案,最简单、最可靠的方法是将视频转换为一系列按顺序编号的图像文件,通常称为图像序列。然后可以通过 skimage.io.imread_collection 将图像文件读入 ImageCollection。将视频转换为帧可以通过 ImageJ 完成,这是一个来自生物成像社区的跨平台、基于GUI的程序,或者通过 FFmpeg,这是一个用于操作视频文件的强大命令行工具。

在 FFmpeg 中,以下命令从视频的每一帧生成一个图像文件。文件编号为五位数,左侧用零填充。

ffmpeg -i "video.mov" -f image2 "video-frame%05d.png"

更多信息可以在 FFmpeg 图像序列教程 中找到。

生成图像序列有一些缺点:它们可能体积庞大且难以管理,生成它们也需要一些时间。通常更倾向于直接处理原始视频文件。为了更直接的解决方案,我们需要从Python执行FFmpeg或LibAV来读取视频帧。FFmpeg和LibAV是两个大型的开源项目,能够解码来自各种格式的视频。从Python中使用它们有几种方法。不幸的是,每种方法都有一些缺点。

7.2. PyAV#

PyAV 使用 FFmpeg(或 LibAV)的库直接从视频文件中读取图像数据。它通过 Cython 绑定调用这些库,因此速度非常快。

import av
v = av.open('path/to/video.mov')

PyAV 的 API 反映了视频文件中帧的存储方式。

import numpy as np
for packet in container.demux():
    for frame in packet.decode():
        if frame.type == 'video':
            img = frame.to_image()  # PIL/Pillow image
            arr = np.asarray(img)  # numpy array
            # Do something!

7.3. 向 PyAV 添加随机访问#

PIMS 中的 Video 类调用了 PyAV,并增加了额外的功能来解决科学应用中的一个常见问题,即通过帧号访问视频。视频文件格式设计为通过时间进行近似搜索,并且不支持高效地查找特定帧号的方法。PIMS 通过解码(但不读取)整个视频并生成一个支持按帧索引的内部目录,来添加这一缺失的功能。

import pims
v = pims.Video('path/to/video.mov')
v[-1]  # a 2D numpy array representing the last frame

7.4. MoviePy#

Moviepy 通过子进程调用 FFmpeg,将解码后的视频从 FFmpeg 通过管道传输到 RAM 中,并读取出来。这种方法简单直接,但可能不够稳定,并且对于超过可用 RAM 的大型视频来说不可行。如果安装了 FFmpeg,它在所有平台上都能工作。

由于它不链接到 FFmpeg 的底层库,因此安装更容易,但速度大约 慢一半

from moviepy.editor import VideoFileClip
myclip = VideoFileClip("some_video.avi")

7.5. Imageio#

Imageio 采用了与 MoviePy 相同的方法。它还支持多种其他图像文件格式。

import imageio
filename = '/tmp/file.mp4'
vid = imageio.get_reader(filename,  'ffmpeg')

for image in vid.iter_data():
    print(image.mean())

metadata = vid.get_meta_data()

7.6. OpenCV#

最后,另一个解决方案是 OpenCV 中的 VideoReader 类,它与 FFmpeg 有绑定。如果你因为其他原因需要使用 OpenCV,那么这可能是最合适的方法。