如何在Python中基于帧时间戳写入可变帧率视频?
解决Python中用自定义时间戳生成可变帧率(VFR)视频的问题
我之前也踩过这个坑!OpenCV和scikit-video确实不太支持直接写入带自定义时间戳的可变帧率视频——它们默认都是按你初始化时设置的固定帧率来计算每帧的显示时间,完全忽略你自己的时间戳数据。不过有两个非常靠谱的方案可以实现你的需求,都是基于FFmpeg生态的(毕竟FFmpeg才是处理视频格式的天花板):
方案一:用FFmpeg命令行 + 临时帧文件
这个方法简单直接,不需要额外的Python库(除了OpenCV用来存帧),适合快速实现:
步骤说明
- 把你的每一帧和对应的时间戳保存成两个部分:
- 按顺序命名的帧图片(比如
frame_000001.png、frame_000002.png) - 一个时间戳文本文件,每行对应一帧的时间戳(转成微秒,FFmpeg默认用这个单位)
- 按顺序命名的帧图片(比如
- 调用FFmpeg命令,通过
-timestamps_file参数指定时间戳文件,让FFmpeg按照我们的时间戳来生成VFR视频。
代码示例
import cv2 import subprocess import os # 假设你已经有了frames列表,每个元素是(OpenCV格式的帧, 相对时间戳(秒)) frames = [...] # 替换成你的实际帧数据 # 创建临时文件夹存放帧和时间戳文件 temp_dir = "temp_vfr_frames" os.makedirs(temp_dir, exist_ok=True) timestamp_path = os.path.join(temp_dir, "timestamps.txt") # 保存帧和时间戳 with open(timestamp_path, "w", encoding="utf-8") as ts_file: for idx, (frame, ts_sec) in enumerate(frames): # 保存帧为PNG(画质无损,避免压缩失真) frame_filename = os.path.join(temp_dir, f"frame_{idx:06d}.png") cv2.imwrite(frame_filename, frame) # 写入时间戳(转成微秒) ts_us = int(ts_sec * 1_000_000) ts_file.write(f"{ts_us}\n") # 调用FFmpeg生成VFR视频 output_path = "vfr_output.mp4" ffmpeg_cmd = [ "ffmpeg", "-f", "image2", "-i", os.path.join(temp_dir, "frame_%06d.png"), # 匹配帧文件名 "-timestamps_file", timestamp_path, "-c:v", "libx264", # H.264编码,兼容性好 "-crf", "23", # 画质参数,数值越小画质越好(18-28是常用范围) "-pix_fmt", "yuv420p", # 兼容大部分播放器 "-y", # 覆盖已有输出文件 output_path ] # 执行命令 subprocess.run(ffmpeg_cmd, check=True, capture_output=True) # 清理临时文件 for file in os.listdir(temp_dir): os.remove(os.path.join(temp_dir, file)) os.rmdir(temp_dir)
方案二:用PyAV(FFmpeg的Python绑定)
如果不想折腾命令行和临时文件,可以用PyAV——它是FFmpeg的官方Python包装,能直接在代码里处理帧和时间戳,集成度更高:
步骤说明
- 先安装PyAV:
pip install av - 创建视频容器和流,设置时间基为微秒(方便计算时间戳)
- 遍历帧,将每个帧的
pts(显示时间戳)设置为我们的自定义时间戳,然后编码写入容器。
代码示例
import av import numpy as np # 假设frames是[(numpy格式的帧, 相对时间戳(秒)), ...] frames = [...] # 替换成你的实际帧数据 output_path = "vfr_output.mp4" # 打开输出容器 with av.open(output_path, mode="w") as container: # 创建视频流,这里rate设为None表示可变帧率 stream = container.add_stream("h264", rate=None) # 设置视频分辨率(从第一帧获取) first_frame = frames[0][0] stream.width = first_frame.shape[1] stream.height = first_frame.shape[0] stream.pix_fmt = "yuv420p" # 兼容播放器 stream.time_base = av.time_base.US # 设置时间基为微秒 for frame_np, ts_sec in frames: # 将numpy帧转换为AVFrame(注意OpenCV是BGR,PyAV默认用RGB,所以转格式) av_frame = av.VideoFrame.from_ndarray(frame_np, format="bgr24") # 设置显示时间戳:把秒转成微秒,作为pts值 av_frame.pts = int(ts_sec * 1_000_000) # 编码帧并写入容器 for packet in stream.encode(av_frame): container.mux(packet) # 刷新编码器,确保所有帧都写入 for packet in stream.encode(): container.mux(packet)
为什么OpenCV/scikit-video不行?
简单来说,这两个库的视频写入模块都是为**固定帧率(CFR)**设计的:
- OpenCV的
VideoWriter会根据你初始化时传入的fps参数,自动计算每帧的时间(比如fps=30的话,每帧间隔1/30秒),完全忽略你自己的时间戳。 - scikit-video底层依赖的是OpenCV或者FFmpeg的简化接口,同样没有暴露设置自定义时间戳的API。
所以想做VFR视频,还是得靠FFmpeg生态的工具~
内容的提问来源于stack exchange,提问作者Amit_JCI




