Rust 里给模型抽帧的三种写法:shell-out ffmpeg、手写解码循环、还是进程内一行

📅 2026/7/26 6:52:56 👁️ 阅读次数 📝 编程学习
Rust 里给模型抽帧的三种写法:shell-out ffmpeg、手写解码循环、还是进程内一行

场景很常见:你在用 candle / burn / ort 做 Rust 侧的推理,要把一段视频喂给模型——第一步永远是拿到帧。而今天在 Rust 里拿帧,主流是两条路。一条是 Command::new("ffmpeg") 把帧写成一堆 PNG 再读回来,或者解析 stdout;另一条是用底层绑定库手写 send_packet / receive_frame 那条解码循环,40 行起步,还得自己管 EAGAIN、自己把 YUV 转成 RGB。第二条难到什么程度?这个品类长出了一整个「帧抓取」的作坊——你搜「rust extract frame from video」,排在前面的权威答案往往是某个 gist。一件这么基础的事,十年了还没有一个顺手的标准答案。

这篇讲第三条路:用 ez-ffmpeg 0.15 的 FrameExtractor,在同一个进程内把帧抽出来,一行调用,拿到的是紧密打包、可直接喂 ndarray / 张量的 RGB 字节。读完你会得到一段可运行的代码、一张采样策略速查表,以及两个真正的干货:为什么你的缩略图颜色可能一直是偏的,以及这条路的速度到底是什么水平(有可复现的数字)。

老办法:要么穿过子进程,要么手写解码循环

先把丑的摆出来。shell-out 那条,产物是文件时最省,但帧要回到内存就得穿过进程边界——落盘 PNG 再读,或者 -f rawvideo 管道自己切字节。手写解码循环那条不穿进程,但样板代码是这样的量级:开封装器、找视频流、建解码器、send_packet / receive_frame 配对、处理 EAGAIN、再用 swscale 把每帧从 YUV 转成 RGB——每一步都能单独写错。而且转 RGB 这一步藏着个坑:很多手写路径(以及一些库)对 HD 视频也套用 BT.601 的转换系数,结果饱和的红和绿会整体偏色,肉眼能看出来,却又不会报错。这个坑下面单独说。

不是说这两条路错。产物是文件、命令现成,CLI 今天仍是对的答案;要逐 packet 的精细控制,底层绑定库无可替代。它们只是没把「进程内、一行、色彩正确地拿到一批帧」这件具体的事做顺。

FrameExtractor:一行拿到一批帧

依赖一行(它仍通过 ffmpeg-next 链接 libav,所以 FFmpeg 7.1–8.x 该装还得装):

[dependencies]
ez-ffmpeg = "0.15"   # needs FFmpeg 7.1-8.x installed (links libav)

抽 32 帧喂给一个 VLM——注意 UniformN,它按显示时间在整段视频上均匀取 N 帧,是 CLIP / VLM 这类固定预算管线要的那个原语:

use ez_ffmpeg::frame_export::{FrameExtractor, Sampling};// 沿整段时长均匀取 32 帧,缩到 224 宽(高按比例推导)。
let frames = FrameExtractor::new("input.mp4").sampling(Sampling::UniformN(32)).width(224).collect_frames()?; // 一次性收成 Vec<VideoFrame>for f in &frames {// f.as_bytes() 是紧密打包、无行填充、自上而下的 RGB24;// width*height*3 字节,可直接做成 ndarray 视图或张量再送去预处理。let (w, h) = (f.width(), f.height());let rgb: &[u8] = f.as_bytes();// your_preprocess(rgb, w, h) ...println!("frame #{:>2} pts={:?}us {}x{}", f.index(), f.pts_us(), w, h);
}

三处值得点一下。new(input) 接受路径、URL 或任何能转成 Input 的东西;像素布局默认 Rgb24,也可换 Rgba32 / Gray8width(224) 只给宽,高就按原始宽高比推导(内部是 scale=224:-2);想固定另一边就用 height,两个都给就是精确尺寸。collect_frames() 一次性收成 Vec;换成 frames() 拿到的是一个流式迭代器(Iterator<Item = Result<VideoFrame>>),边解码边消费,不把整段视频的帧全压进内存——大文件抽稠帧时用它。

这套 API 的形状不是随手设计的,而是刻意贴着 FFmpeg CLI:链式调用读起来就像一条命令,start_time_us / duration_us 对应 -ss / -t(秒换成微秒),内部拼的滤镜图也是你在命令行敲的 scale=…,format=rgb24 这一类串(外加显式的色彩管理参数)。这条「把 CLI 原样搬进 Rust 进程」的主线,本系列的生态综述篇会单独展开,这里只提一句:你攒的 ffmpeg 命令行直觉,在这里不作废。

采样:要几帧、要哪些帧

抽帧的一半问题是「抽哪些」。Sampling 把常见策略摆全了:

策略 语义 典型用途
All 每一个解码帧(默认) 稠密导出、逐帧分析
EveryNth(n) 每 n 帧取 1 帧 按帧率抽稀
EverySec(k) 每 k 秒取 1 帧(浮点秒) 时间轴略缩图、预览条
KeyframesOnly 只要关键帧 镜头/场景代理帧,解码期就快
UniformN(n) 沿时长均匀取恰好 n 帧 VLM / CLIP 固定预算输入

两个值得单说。KeyframesOnly 会给解码器钉上 skip_frame=nokey,非关键帧在解码阶段就被跳过——不是解完再丢,是根本不解,所以它快在刀刃上。UniformN(n) 保证恰好 n 帧:短到装不下 n 个不同帧的输入,会就近重复补齐(重复帧保留各自源帧的 pts_us),这样模型管线被许诺的「就要 32 帧」永远兑现,不用在调用方写补齐逻辑。

色彩:HD 默认走 BT.709,这不是小事

回到那个偏色的坑。YUV 转 RGB 要选一套系数:SD 视频用 BT.601,HD 视频用 BT.709,选错了饱和色就偏。很多「解码成 RGB」的快捷路径对所有输入一律套 BT.601——SD 没问题,一到 1080p 就偏,而且不报错,你只有把结果贴出来才看得见。

FrameExtractor 默认按帧自带的色彩标签来转(ColorPolicy::Tagged):打了 BT.709 标签的 HD 帧就按 BT.709 转,不猜、不一刀切。这也是这个模块笔者最愿意为它背书的一个行为。作个对照:老版本 PyAV 的 to_ndarray('rgb24') 是一刀切 BT.601(这是「老版本」的说法,新版已改,别当它现在还这样)。碰到没打标签的帧,还有 TaggedOrResolutionGuess 按分辨率猜(≥720 高判 BT.709),或者 Force { matrix, range } 全程强制指定。默认那档,大多数人不用碰,但知道它在替你做对这件事,值得。

什么场景别用它

老规矩,先往外划拉,别只往自己碗里说。

  • 产物是磁盘上的缩略图,命令现成、只转一次。 ffmpeg -i in.mp4 -vf "select=..." -vsync vfr out_%03d.png 一行的事,为它引 crate 是绕路。
  • 要帧、但不想碰 FFI 和链接,子进程可接受。 ffmpeg-sidecar(月下载约 13 万)把任意视频封装成 RGB 帧迭代器,不链 libav,体验很顺——代价是运行环境要有 ffmpeg 二进制、帧要穿进程边界。
  • 只做视频、能接受 WIP。 video-rs 十行内就能迭代 RGB ndarray(月下载约 3.2 万);它没有音频 API、自述 work-in-progress,但视频单科很能打。说清楚:Rust 从来不是「拿不到解码帧」,能拿的路子好几条;FrameExtractor 的差异点是把这件事做成进程内、带采样策略和色彩管理的一行式 API。
  • 要 decord 那种 GPU 批量解码。 那是另一个能力档,本文不宣称对等。

还有三条实话收在这:一,编译和链接的痛,ez-ffmpeg 一点没消除——它仍链 libav,该过的链接关一关不少;二,项目约 340 star,仍是早期阶段,「safe 的 Rust API」不等于「没有 C 的 CVE」,底层还是 libav;三,frame_export 整个模块——FrameExtractor 连同姐妹 API SampleExtractor(音频→16k PCM)、VideoWriter(帧推入编码)——在文档里仍标注 experimental:0.15 定下的是默认精度、采样这些行为语义,API 形状仍可能小调。

写在最后

帧不出进程、一行拿到、按色彩标签转对、转换回到 swscale 快路径——十年前那个「先渲染成图再用 CLI 拼」的作坊,到这一步可以歇了。你在 Rust 里做 ML,抽帧不该还要绕出去。

可运行示例都在仓库:examples/uniform_thumbnails(UniformN 配 4×3 略缩图网格)、examples/extract_rgb_frames(抽 RGB 存 PPM)、examples/keyframe_thumbnails(关键帧代理)、examples/frame_sampling(各采样策略对照)。项目地址:github.com/YeautyYE/ez-ffmpeg。

你现在的 Rust ML 管线,抽帧是走 shell-out、手写解码,还是别的路?欢迎评论区聊聊;如有描述不准之处,欢迎指正。