如何在Python中从视频中提取帧?详细实现教程

2021年11月17日14:22:54 发表评论 1,495 次浏览

Python如何从视频中提取帧?本文教你使用 Python 中的 OpenCV 或 MoviePy 库制作两种不同的方法来从带有时间戳的视频中提取帧。

你可能已经知道,视频由一系列图像组成。这些图像被称为帧,并以一定的速率一张一张地连续播放,这将被人眼识别为运动。

如何在Python中从视频中提取帧?在本教程中,你将学习两种在 Python 中从视频文件中提取帧的方法。首先,我们将讨论如何使用著名的OpenCV库来做到这一点。之后,我们将探索使用MoviePy 库提取帧的另一种方法。

首先,让我们安装库:

$ pip install python-opencv moviepy

方法 1:使用 OpenCV 提取帧

Python从视频中提取帧示例:我将创建extract_frames_opencv.py文件并导入必要的模块:

from datetime import timedelta
import cv2
import numpy as np
import os

由于并非所有视频的长度和FPS都相同,我们将定义一个参数来调整我们想要每秒提取和保存的帧数:

# i.e if video of duration 30 seconds, saves 10 frame per second = 300 frames saved in total
SAVING_FRAMES_PER_SECOND = 10

Python如何从视频中提取帧?我们将在这两种方法上使用此参数。例如,如果像现在一样设置为 10,即使视频 FPS 为 24,它也只会保存每秒 10 帧的视频。如果视频有 30 秒的持续时间,那么总共将保存 300 帧. 你也可以将此参数设置为 0.5,即每 2 秒保存一帧,依此类推。

接下来,让我们定义两个辅助函数:

def format_timedelta(td):
    """Utility function to format timedelta objects in a cool way (e.g 00:00:20.05) 
    omitting microseconds and retaining milliseconds"""
    result = str(td)
    try:
        result, ms = result.split(".")
    except ValueError:
        return result + ".00".replace(":", "-")
    ms = int(ms)
    ms = round(ms / 1e4)
    return f"{result}.{ms:02}".replace(":", "-")


def get_saving_frames_durations(cap, saving_fps):
    """A function that returns the list of durations where to save the frames"""
    s = []
    # get the clip duration by dividing number of frames by the number of frames per second
    clip_duration = cap.get(cv2.CAP_PROP_FRAME_COUNT) / cap.get(cv2.CAP_PROP_FPS)
    # use np.arange() to make floating-point steps
    for i in np.arange(0, clip_duration, 1 / saving_fps):
        s.append(i)
    return s

如何在Python中从视频中提取帧?该format_timedelta()函数接受一个timedelta对象并返回一个漂亮的字符串表示,以毫秒为单位并省略微秒。

get_saving_frames_durations()函数接受VideoCapture来自 OpenCV的对象和我们之前讨论的保存参数,并返回我们应该保存帧的持续时间点列表。

现在我们有了这些辅助函数,让我们定义主函数并解释它:

def main(video_file):
    filename, _ = os.path.splitext(video_file)
    filename += "-opencv"
    # make a folder by the name of the video file
    if not os.path.isdir(filename):
        os.mkdir(filename)
    # read the video file    
    cap = cv2.VideoCapture(video_file)
    # get the FPS of the video
    fps = cap.get(cv2.CAP_PROP_FPS)
    # if the SAVING_FRAMES_PER_SECOND is above video FPS, then set it to FPS (as maximum)
    saving_frames_per_second = min(fps, SAVING_FRAMES_PER_SECOND)
    # get the list of duration spots to save
    saving_frames_durations = get_saving_frames_durations(cap, saving_frames_per_second)
    # start the loop
    count = 0
    while True:
        is_read, frame = cap.read()
        if not is_read:
            # break out of the loop if there are no frames to read
            break
        # get the duration by dividing the frame count by the FPS
        frame_duration = count / fps
        try:
            # get the earliest duration to save
            closest_duration = saving_frames_durations[0]
        except IndexError:
            # the list is empty, all duration frames were saved
            break
        if frame_duration >= closest_duration:
            # if closest duration is less than or equals the frame duration, 
            # then save the frame
            frame_duration_formatted = format_timedelta(timedelta(seconds=frame_duration))
            cv2.imwrite(os.path.join(filename, f"frame{frame_duration_formatted}.jpg"), frame) 
            # drop the duration spot from the list, since this duration spot is already saved
            try:
                saving_frames_durations.pop(0)
            except IndexError:
                pass
        # increment the frame count
        count += 1

上面的函数看起来很复杂,其实不然,这就是我们要做的:

  • 首先,我们创建文件名变量,它是我们要创建和保存框架的文件夹名称,我们附加"-opencv"只是为了区分方法,但你可以删除它。
  • 然后,我们使用该os.mkdir()函数创建文件夹(如果尚未创建)。
  • 之后,我们使用 读取视频文件cv2.VideoCapture,并使用cap.get()方法获取 FPS并传递 FPS 的代码,即cv2.CAP_PROP_FPS.
  • 我们将每秒保存帧数设置为实际视频 FPS 和我们参数中的最小值。因此,我们确保不能绕过比实际视频 fps 更高的保存 fps。
  • 得到保存时长后,我们进入读取帧的循环,只有当我们确定时长在我们的saving_frames_durations列表中时才进行保存。我们使用 保存帧cv2.imwrite(),并将帧名称设置为实际持续时间。

定义主要Python从视频中提取帧示例代码:

if __name__ == "__main__":
    import sys
    video_file = sys.argv[1]
    main(video_file)

由于我们使用命令行参数传递视频文件,让我们运行它:

$ python extract_frames_opencv.py zoo.mp4

执行上述命令后,"zoo-opencv"会创建一个新文件夹,其中包含以下内容:如何在Python中从视频中提取帧?详细实现教程

如你所见,帧与文件名中的时间戳一起保存。

相关: 如何在 Python 中从视频中提取音频

方法 2:使用 MoviePy 提取帧

Python如何从视频中提取帧?在这个方法中,我们不会使用 OpenCV,而是使用另一个名为 MoviePy 的库,我将创建一个名为的文件extract_frames_moviepy.py并导入必要的模块:

from moviepy.editor import VideoFileClip
import numpy as np
import os
from datetime import timedelta

与第一种方法一样,我们也将在SAVING_FRAMES_PER_SECOND这里使用参数:

# i.e if video of duration 30 seconds, saves 10 frame per second = 300 frames saved in total
SAVING_FRAMES_PER_SECOND = 10

请参阅本教程的第一部分以了解它的确切含义。和以前一样,我们也需要这个format_timedelta()函数:

def format_timedelta(td):
    """Utility function to format timedelta objects in a cool way (e.g 00:00:20.05) 
    omitting microseconds and retaining milliseconds"""
    result = str(td)
    try:
        result, ms = result.split(".")
    except ValueError:
        return result + ".00".replace(":", "-")
    ms = int(ms)
    ms = round(ms / 1e4)
    return f"{result}.{ms:02}".replace(":", "-")

现在转到主函数:

def main(video_file):
    # load the video clip
    video_clip = VideoFileClip(video_file)
    # make a folder by the name of the video file
    filename, _ = os.path.splitext(video_file)
    filename += "-moviepy"
    if not os.path.isdir(filename):
        os.mkdir(filename)

    # if the SAVING_FRAMES_PER_SECOND is above video FPS, then set it to FPS (as maximum)
    saving_frames_per_second = min(video_clip.fps, SAVING_FRAMES_PER_SECOND)
    # if SAVING_FRAMES_PER_SECOND is set to 0, step is 1/fps, else 1/SAVING_FRAMES_PER_SECOND
    step = 1 / video_clip.fps if saving_frames_per_second == 0 else 1 / saving_frames_per_second
    # iterate over each possible frame
    for current_duration in np.arange(0, video_clip.duration, step):
        # format the file name and save it
        frame_duration_formatted = format_timedelta(timedelta(seconds=current_duration)).replace(":", "-")
        frame_filename = os.path.join(filename, f"frame{frame_duration_formatted}.jpg")
        # save the frame with the current duration
        video_clip.save_frame(frame_filename, current_duration)

如何在Python中从视频中提取帧?你可能已经注意到,此方法需要的代码较少。首先,我们使用VideoFileClip()类加载我们的视频剪辑,我们创建我们的文件夹并确保保存的 fps 小于或等于视频 fps。

然后我们定义我们的循环步长,即 1 除以保存的 fps,如果我们将 设置SAVING_FRAMES_PER_SECOND为 10,那么步长将为 0.1(即每 0.1 秒保存帧)。

这里的区别在于VideoFileClip对象有save_frame()方法,该方法接受两个参数:帧文件名和要保存的帧的持续时间。所以我们所做的是我们循环使用np.arange()(常规range()函数的浮点版本)在我们想要的每一帧上采取步骤,并相应地调用该save_frame()方法。

下面是主要对Python从视频中提取帧示例代码:

if __name__ == "__main__":
    import sys
    video_file = sys.argv[1]
    main(video_file)

让我们测试一下: 

$ python extract_frames_moviepy.py zoo.mp4

几秒钟后,zoo-moviepy文件夹被创建,帧被保存在该文件夹中。

结论

Python如何从视频中提取帧?在我的案例中使用这两种方法后,我注意到方法一(使用 OpenCV)在时间执行方面更快,但比 MoviePy 保存更大的图像。

对于该演示视频,使用第二种方法(使用 MoviePy)时190帧的大小为2.8MB,使用 OpenCV 时为5.46MB。然而,MoviePy 方法的持续时间为2.3秒,而 OpenCV 需要大约0.6秒。

话虽如此,我已经为你提供了两种在 Python 中从视频中提取帧的方法,你可以选择最适合你的方法。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: