获取视频中每个帧的时间戳

mac*_*ery 9 python video opencv frame-rate

我已经用我编写的Android 5.2应用程序从平板电脑的前置摄像头录制了几个视频.我已经为每个视频存储了以毫秒(Unix时间)为单位的开始时间戳.

不幸的是,每个视频都有不同的帧速率(范围从20到30).使用OpenCV,我可以获得每个视频的帧速率:

import cv2
video = cv2.VideoCapture(videoFile)
fps = video.get(cv2.CAP_PROP_FPS)
Run Code Online (Sandbox Code Playgroud)

这很好用,理论上我只能为视频中的每一帧添加1000/fps(由于毫秒).但这假设帧率在整个录制过程中保持稳定.我不知道是不是这样.

Python中是否有可能获得视频中每帧的时间戳(以毫秒为单位),而与帧速率无关?

小智 21

这是一个简化版本,仅读取视频并打印出帧号及其时间戳。

import cv2

cap = cv2.VideoCapture('path_to_video/video_filename.avi')

frame_no = 0
while(cap.isOpened()):
    frame_exists, curr_frame = cap.read()
    if frame_exists:
        print("for frame : " + str(frame_no) + "   timestamp is: ", str(cap.get(cv2.CAP_PROP_POS_MSEC)))
    else:
        break
    frame_no += 1

cap.release()
Run Code Online (Sandbox Code Playgroud)

这给出了如下所示的输出:

for frame : 0   timestamp is:  0.0
for frame : 1   timestamp is:  40.0
for frame : 2   timestamp is:  80.0
for frame : 3   timestamp is:  120.0
for frame : 4   timestamp is:  160.0
for frame : 5   timestamp is:  200.0
for frame : 6   timestamp is:  240.0
for frame : 7   timestamp is:  280.0
for frame : 8   timestamp is:  320.0
for frame : 9   timestamp is:  360.0
for frame : 10   timestamp is:  400.0
for frame : 11   timestamp is:  440.0
for frame : 12   timestamp is:  480.0
...
Run Code Online (Sandbox Code Playgroud)


alk*_*asm 14

你想要的cv2.CAP_PROP_POS_MSEC.在此处查看所有不同的捕获属性.

编辑:实际上,正如DanMašek向我指出的那样,当你抓住那个属性时,看起来OpenCV 正在进行这种计算(至少假设你正在使用FFMPEG):

case CV_FFMPEG_CAP_PROP_POS_MSEC:
    return 1000.0*(double)frame_number/get_fps();
Run Code Online (Sandbox Code Playgroud)

所以看起来你总是依赖于恒定的帧率假设.但是,即使假设帧速率恒定,重要的是乘以帧数而不是仅仅继续添加1000/fps.当你反复添加花车时会出现错误,这些浮动通过长视频可以产生很大的不同.例如:

import cv2

cap = cv2.VideoCapture('vancouver2.mp4')
fps = cap.get(cv2.CAP_PROP_FPS)

timestamps = [cap.get(cv2.CAP_PROP_POS_MSEC)]
calc_timestamps = [0.0]

while(cap.isOpened()):
    frame_exists, curr_frame = cap.read()
    if frame_exists:
        timestamps.append(cap.get(cv2.CAP_PROP_POS_MSEC))
        calc_timestamps.append(calc_timestamps[-1] + 1000/fps)
    else:
        break

cap.release()

for i, (ts, cts) in enumerate(zip(timestamps, calc_timestamps)):
    print('Frame %d difference:'%i, abs(ts - cts))
Run Code Online (Sandbox Code Playgroud)

第0帧差:0.0
1帧差:0.0
2帧差:0.0
3帧差:1.4210854715202004e-14
帧4的区别:0.011111111111091532
框架5的区别:0.011111111111091532
框架6的区别:0.011111111111091532
框架7的区别:0.011111111111119953
框架8差:0.022222222222183063
帧9差异:0.022222222222183063
...
框架294差异:0.8111111111411446

这当然是毫秒,所以也许它看起来不那么大.但是在这里,我在计算中差不多1毫秒,这只是一个11秒的视频.无论如何,使用这个属性更容易.


jer*_*ron 5

我对多个库做了一些测试。

import av
import cv2
import json
import os
import shutil
import sys
import subprocess
import time
from decimal import Decimal
from decord import VideoReader
from ffms2 import VideoSource
from moviepy.editor import VideoFileClip
from typing import List


def with_movie_py(video: str) -> List[int]:
    """
    Link: https://pypi.org/project/moviepy/
    My comments:
        The timestamps I get are not good compared to gMKVExtractGUI or ffms2. (I only tried with VFR video)

    Parameters:
        video (str): Video path
    Returns:
        List of timestamps in ms
    """
    vid = VideoFileClip(video)

    timestamps = [
        round(tstamp * 1000) for tstamp, frame in vid.iter_frames(with_times=True)
    ]

    return timestamps


def with_cv2(video: str) -> List[int]:
    """
    Link: https://pypi.org/project/opencv-python/
    My comments:
        I don't know why, but the last 4 or 5 timestamps are equal to 0 when they should not.
        Also, cv2 is slow. It took my computer 132 seconds to process the video.


    Parameters:
        video (str): Video path
    Returns:
        List of timestamps in ms
    """
    timestamps = []
    cap = cv2.VideoCapture(video)

    while cap.isOpened():
        frame_exists, curr_frame = cap.read()
        if frame_exists:
            timestamps.append(round(cap.get(cv2.CAP_PROP_POS_MSEC)))
        else:
            break

    cap.release()

    return timestamps


def with_pyffms2(video: str) -> List[int]:
    """
    Link: https://pypi.org/project/ffms2/
    My comments:
        Works really well, but it doesn't install ffms2 automatically, so you need to do it by yourself.
        The easiest way is to install Vapoursynth and use it to install ffms2.
        Also, the library doesn't seems to be really maintained.

    Parameters:
        video (str): Video path
    Returns:
        List of timestamps in ms
    """
    video_source = VideoSource(video)

    # You can also do: video_source.track.timecodes
    timestamps = [
        int(
            (frame.PTS * video_source.track.time_base.numerator)
            / video_source.track.time_base.denominator
        )
        for frame in video_source.track.frame_info_list
    ]

    return timestamps


def with_decord(video: str) -> List[int]:
    """
    Link: https://github.com/dmlc/decord
    My comments:
        Works really well, but it seems to only work with mkv and mp4 file.
        Important, Decord seems to automatically normalise the timestamps which can cause many issue: https://github.com/dmlc/decord/issues/238
        Mp4 file can have a +- 1 ms difference with ffms2, but it is acceptable.

    Parameters:
        video (str): Video path
    Returns:
        List of timestamps in ms
    """
    vr = VideoReader(video)

    timestamps = vr.get_frame_timestamp(range(len(vr)))
    timestamps = (timestamps[:, 0] * 1000).round().astype(int).tolist()

    return timestamps


def with_pyav(video: str, index: int = 0) -> List[int]:
    """
    Link: https://pypi.org/project/av/
    My comments:
        Works really well, but it is slower than ffprobe.
        The big advantage is that ffmpeg does not have to be installed on the computer, because pyav installs it automatically

    Parameters:
        video (str): Video path
        index (int): Stream index of the video.
    Returns:
        List of timestamps in ms
    """
    container = av.open(video)
    video = container.streams.get(index)[0]

    if video.type != "video":
            raise ValueError(
                f'The index {index} is not a video stream. It is an {video.type} stream.'
            )

    av_timestamps = [
        int(packet.pts * video.time_base * 1000) for packet in container.demux(video) if packet.pts is not None
    ]

    container.close()
    av_timestamps.sort()

    return av_timestamps


def with_ffprobe(video_path: str, index: int = 0) -> List[int]:
    """
    Link: https://ffmpeg.org/ffprobe.html
    My comments:
        Works really well, but the user need to have FFMpeg in his environment variables.

    Parameters:
        video (str): Video path
        index (int): Index of the stream of the video
    Returns:
        List of timestamps in ms
    """

    def get_pts(packets) -> List[int]:
        pts: List[int] = []

        for packet in packets:
            pts.append(int(Decimal(packet["pts_time"]) * 1000))

        pts.sort()
        return pts

    # Verify if ffprobe is installed
    if shutil.which("ffprobe") is None:
        raise Exception("ffprobe is not in the environment variable.")

    # Getting video absolute path and checking for its existance
    if not os.path.isabs(video_path):
        dirname = os.path.dirname(os.path.abspath(sys.argv[0]))
        video_path = os.path.join(dirname, video_path)
    if not os.path.isfile(video_path):
        raise FileNotFoundError(f'Invalid path for the video file: "{video_path}"')

    cmd = f'ffprobe -select_streams {index} -show_entries packet=pts_time:stream=codec_type "{video_path}" -print_format json'
    ffprobeOutput = subprocess.run(cmd, capture_output=True, text=True)
    ffprobeOutput = json.loads(ffprobeOutput.stdout)

    if len(ffprobeOutput) == 0:
        raise Exception(
            f"The file {video_path} is not a video file or the file does not exist."
        )

    if len(ffprobeOutput["streams"]) == 0:
        raise ValueError(f"The index {index} is not in the file {video_path}.")

    if ffprobeOutput["streams"][0]["codec_type"] != "video":
        raise ValueError(
            f'The index {index} is not a video stream. It is an {ffprobeOutput["streams"][0]["codec_type"]} stream.'
        )

    return get_pts(ffprobeOutput["packets"])


def main():
    video = r"WRITE_YOUR_VIDEO_PATH"

    start = time.process_time()
    movie_py_timestamps = with_movie_py(video)
    print(f"With Movie py {time.process_time() - start} seconds")

    start = time.process_time()
    cv2_timestamps = with_cv2(video)
    print(f"With cv2 {time.process_time() - start} seconds")

    start = time.process_time()
    ffms2_timestamps = with_pyffms2(video)
    print(f"With ffms2 {time.process_time() - start} seconds")

    start = time.process_time()
    decord_timestamps = with_decord(video)
    print(f"With decord {time.process_time() - start} seconds")

    start = time.process_time()
    av_timestamps = with_pyav(video)
    print(f"With av {time.process_time() - start} seconds")

    start = time.process_time()
    ffprobe_timestamps = with_ffprobe(video)
    print(f"With ffprobe {time.process_time() - start} seconds")


if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)

以下是获取 24 分钟 mp4 的时间戳所需的时间。

With Movie py 11.421875 seconds
With cv2 131.890625 seconds
With ffms2 0.640625 seconds
With decord 0.328125 seconds
With av 0.6875 seconds
With ffprobe 0.21875 seconds
Run Code Online (Sandbox Code Playgroud)