手上有个说是带透明背景的 WebM,我想在交给下一个环节之前先弄清它到底带没带,不想靠猜。这篇只干一件事:用一条能复制的命令确认 WebM 里有没有透明通道,再查清楚转成剪辑中间格式时透明会在哪一步丢掉。我不是科班出身,对视频编码的认识都是从表现一点点推出来的,所以每个结论后面都跟着实测数字和命令输出,推测的地方我会直接写成推测。
摘要
同一个 8 秒、540×960 的透明 WebM,FFmpeg 默认解码读到的 alpha 在 240 帧里全部是 255,整条视频都不透明。只要把 -c:v libvpx-vp9 写在输入文件前面,同一个文件逐帧 alpha 均值就落在 42.9292 到 51.5385 之间而没有一帧是 255。这个差别不会报错,也没有警告。用默认解码去转 ProRes 4444 或 QuickTime Animation,文件照样生成、照样能播,只是整条视频已经不透明了。
判断一个 WebM 带没带透明不能只看 alpha_mode 标签。我用 FFmpeg 重新编码构造过两个反例:一个有标签没有透明数据,一个有透明数据没有标签。可靠的办法是数视频块里有没有 BlockAdditions,文中给了一段自己写的手解脚本。转成 ProRes 4444、QuickTime Animation、PNG 序列时,只要解码那一步指定了解码器,三种格式回读的 alpha 都和原文件完全一致,体积是原 WebM 的约 26 到 38 倍。素材里的透明 WebM 由图映 ImgIng 导出,其余全部是 FFmpeg 命令行的事。
版本声明
测试日期是 2026-10-27。机器是 Apple M4,系统是 macOS。FFmpeg 版本是 7.1,来自 Python 包里自带的静态构建。含 libvpx-vp9、prores_ks 和 qtrle。所有数字只对这一段 8 秒素材、这一台机器、单次运行成立。体积一律用实际字节,文中的 MB 按 1,000,000 字节换算。图映界面里的 MB 是 1,048,576 字节,对照界面数字时要留意这个差别。
素材是一段真人口播绿幕视频,来自 VOA Learning English《Filming with the green screen》。维基共享资源标注为公有领域。我截取了开头 8 秒并缩成 540×960,再把它交给视频抠像去导出透明 WebM。导出用的是图映 ImgIng(imging.cn/)。这个输入文件的大小... 2,394,232 字节。
适用边界
先把能带走的和不能带走的分开。能带走的是方法:指定解码器读 alpha、逐帧扫描、数 BlockAdditions、转格式后回读。这四步不依赖具体素材。不能带走的是数字:42.9292 和 62,034,554 这些值只属于这一个文件,换一段素材就会变。
有几件事我没有测,写在这里是为了避免你把它们当成结论。第一是剪辑软件导入透明 WebM 的实际表现。我本机没有装这类软件,也没有读到可靠的官方说明,所以这篇不写任何一款剪辑软件支持或者不支持。第二是 FFmpeg 的版本:我只测了 7.1,别的版本里默认解码是不是也读不到 alpha 我不知道。第三是本机没有 ffprobe,它的 side_data 路线没有试,手解是我自己写的。第四是图映的视频抠像目前处在 Beta 阶段,在电脑浏览器上本地处理,手机上不能用。这篇不评价它的抠像效果,只把它导出的透明 WebM 当作检查对象。
文章目录
一、透明为什么在剪辑软件里消失了
- 文件本身没坏只是没人去读透明层
- 同一帧两种读法差了整层透明
二、怎么用一条命令确认有没有透明
- 先看标签但标签只是线索
- 同一帧并排比较两种解码
- 命令里的几个词各管一件事
- 扫满 240 帧才算确认
三、alpha_mode 标签为什么靠不住
- 标签可以残留也可以丢失
- 要数视频块里的 BlockAdditions
- 手解脚本只做两件事
四、转剪辑中间格式时透明在哪一步丢掉
- 默认解码转出的 ProRes 照样能播
- 看参数分不出有没有透明
- 指定解码器后三种格式都保住透明
五、三种中间格式的体积差多少
- 体积约是透明 WebM 的 26 到 38 倍
- 挑格式要看体积文件数和用途
- 再编码一次透明还在但略有损耗
- H.264 本来就没有透明层
六、拿到透明 WebM 该按什么顺序检查
七、测量这件事自己有哪些坑
- 滤镜链里没有透明平面会直接报错
- 解码器参数放错位置读到的是常数
- 刻度不同的数字不能直接比
- 标签大小写会让人漏查
八、还有哪些问题没有查清
常见问题
- 为什么默认解码读不到透明却不报错
- 只看 alpha_mode 标签行不行
- 转成 ProRes 之前一定要先检查吗
- 转完回读一帧够不够
- 导出的这个透明 WebM 有什么特别吗
参考资料
一、透明为什么在剪辑软件里消失了
文件本身没坏只是没人去读透明层
先说最容易误会的一点:透明没了,不等于文件坏了,也不等于导出时漏了透明。我手上这个文件里的透明数据一直都在,只是默认读它的那一步没有去读。我用同一个文件做了两次对照:一次让 FFmpeg 用自带的解码器去读,一次明确指定 libvpx-vp9。第 60 帧的 alpha 在两次里完全是两个世界。
先把 alpha 说明白。一张普通图片的每个像素有红绿蓝三个数。带透明的图多一个数叫 alpha,0 代表完全透明。255 代表完全不透明,中间的数是半透明。视频是一连串图片而每一帧的每个像素都可以带这个数。透明背景视频的意思就是人物所在的位置 alpha 是 255。背景所在的位置 alpha 是 0,头发边缘和衣服毛边这些地方是中间值。
WebM 里的 VP9 视频带透明,做法有点特别。我从手解的结果推测主视频画面是一路普通的 yuv420p 画面,alpha 被另外编码。作为附加数据挂在每个视频块旁边。这个输入文件里 240 个视频块都挂着附加数据,加起来 1,036,225 字节。约占整个文件 2,394,232 字节的 43%。这是我从字节数推出来的理解。WebM 规范原文怎么写我没有逐条核对过,只当作理解方式而不当作规范结论。
顺着这个理解,问题就好解释了。如果一个解码器只读主画面而不去看附加数据,它交出来的就是一路没有透明信息的视频。后面不管接剪辑还是转码,看到的都是一条不透明的视频。FFmpeg 默认用来解 VP9 的是它自带的实现,在 7.1 这个构建上实测就是这么回事。对其他版本我没有测过。
同一帧两种读法差了整层透明
下面是同一个文件同一帧(第 60 帧)的两种读法。左边是默认解码,右边是指定 libvpx-vp9 解码。右边用棋盘格表示透明区域。

这张图要看什么:左边整块是黑底,人物周围本来该透明的地方被填成了不透明的黑。右边是棋盘格底,人物边缘露出了格子,说明同一帧里真的有透明区域。图底部给了两组数字,左边 alpha 最小、最大、均值都是 255、255、255.0。右边是 0、255、46.76。画面里有两个人:左边是一位站着的男士,右边是正在挥手的女士。两边的人物主体一模一样,差别只在背景。
这一帧的细账我也算过。默认解码读到的 alpha 里不透明像素占 100.00%。指定 libvpx-vp9 之后不透明像素占 15.83%、完全透明占 78.91%、半透明占 5.26%。半透明的那 5.26% 就是头发丝和衣服边缘一圈渐变的位置。人物抠得越细这一圈越多。再看第 150 帧,默认解码的均值还是 255.0。指定解码器是 45.893。这不是某一帧碰巧如此,后面会把整条视频逐帧扫一遍。
默认解码输出成 PNG 的时候,原本透明的位置显示成黑色,图里左边的黑底就是这么来的。我觉得这个细节挺要紧:看到黑底,第一反应很容易是背景没抠干净。可是对这个文件来说,透明数据还在,只是解码这一步没有读。
二、怎么用一条命令确认有没有透明
先看标签但标签只是线索
第一步最简单,用 ffmpeg 看一眼文件信息。下面的命令里文件名我统一写成 clip.webm,换成你自己的就行。
yaml
$ ffmpeg -hide_banner -i clip.webm
Duration: 00:00:08.03, start: 0.000000, bitrate: 2386 kb/s
Stream #0:0(eng): Video: vp9 (Profile 0), yuv420p(tv, smpte170m), 540x960, SAR 1:1 DAR 9:16, 30.30 fps, 30 tbr, 1k tbn (default)
Metadata:
alpha_mode : 1
Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
代码说明:这条命令只读文件信息,不输出新文件。输出里含本机路径的 Input 行我略去了。要看的是视频那一行的像素格式和下面的 Metadata。像素格式写的是 yuv420p 没有字母 a,Metadata 里却有一行 alpha_mode : 1。这两行乍看是打架的:像素格式说没有 alpha,标签却说有 alpha。
其实这两行并不矛盾。像素格式那一行说的是默认解码路径读到的东西,里面没有 alpha 平面才会写 yuv420p。alpha_mode : 1 是容器里记的标签,意思是这条视频声称自己带了 alpha。标签只是作者留的一张便条,便条上写什么和箱子里装什么是两回事。第三章会专门查这一点。这一步我只把它当线索:看到标签就进入下一步,看不到标签也别急着下结论。
还有一个细节:默认解码下 ffmpeg 的这一行不会提示丢了 alpha。它不报错也不警告而是安安静静地给你一路不透明的视频。这是整件事最容易漏过去的地方,也是后面所有检查都要靠数字而不靠肉眼的原因。
同一帧并排比较两种解码
第二步才是正题。对同一个文件的同一帧分别用默认解码和指定 libvpx-vp9 解码,把 alpha 单独取出来算统计量。两条命令成对运行,差别只有一处:第二条在 -i 前面多了 -c:v libvpx-vp9。
csharp
$ ffmpeg -hide_banner -i clip.webm -vf "select=eq(n\,60),format=rgba,alphaextract,signalstats,metadata=print" -an -f null -
lavfi.signalstats.YMIN=255
lavfi.signalstats.YAVG=255
lavfi.signalstats.YMAX=255
$ ffmpeg -hide_banner -c:v libvpx-vp9 -i clip.webm -vf "select=eq(n\,60),format=yuva420p,alphaextract,signalstats,metadata=print" -an -f null -
lavfi.signalstats.YMIN=0
lavfi.signalstats.YAVG=46.7618
lavfi.signalstats.YMAX=255
代码说明:select=eq(n\,60) 只留第 60 帧。alphaextract 把 alpha 单独抽成一张灰度图。signalstats 对这张灰度图算最小值、均值和最大值,metadata=print 把结果打印出来。-f null - 表示不输出文件只跑滤镜。ffmpeg 还会打印很多日志。这里只保留 signalstats 三行,其余省略。
读法很直接。第一条命令三个值都是 255:这一帧的 alpha 平面是一张纯白的图,也就是全不透明。第二条命令最小是 0,最大是 255。均值是 46.7618:里面有完全透明的像素、完全不透明的像素,还有一部分在中间。均值离 0 近得多是因为这帧画面里绝大部分面积是背景。
这一对命令里有两个细节要盯住。一是第二条命令里的 -c:v libvpx-vp9 必须写在 -i 之前,写在后面是另一个意思(第七章有实测)。二是两条命令的 format= 不一样,第一条是 rgba。第二条是 yuva420p。默认解码路径下如果不先 format 就直接 alphaextract,ffmpeg 会报 Could not open encoder ... Invalid argument。原因是解出来的帧里根本没有 alpha 平面可抽。我的理解是,第一条里的 format=rgba 在没有 alpha 的帧上补了一个全 255 的 alpha,滤镜链才跑得通。所以默认路径上数到的 255,不能当成原文件里的值。
命令里的几个词各管一件事
前面的命令里有一串词,非科班的人第一次看会觉得像咒语。我把它们按顺序拆开讲一遍,你换成自己的文件时就知道哪里能改、哪里不能动。
先说像素格式。yuv420p 是视频里最常见的一种,只有明暗和颜色信息。yuva420p 比它多一个字母 a,意思是多带了一层 alpha。我在 ffmpeg -i 的输出里看到的情况是这样的:默认解码下读到的流是 yuv420p,指定 libvpx-vp9 之后读到的流变成了 yuva420p。字母 a 有没有出现,就是最快的一个线索。但它只能说明这一次是怎么读的,说明不了文件本身带不带透明。
再说滤镜链里的三个词。alphaextract 把 alpha 单独抽出来变成一张灰度图,白色是不透明,黑色是透明。signalstats 对一张图做统计,它输出的 YMIN、YAVG、YMAX 本来是亮度的最小值、平均值和最大值。前一步已经把 alpha 变成了灰度图,这三个数在这里就等于 alpha 的最小值、平均值和最大值。select=eq(n\,60) 的意思是只留帧序号等于 60 的那一帧,帧序号在 ffmpeg 里从 0 开始数,文中说的第 60 帧其实是画面里的第 61 张。这点差别不影响结论,默认解码下每一帧都是 255。
再说收尾的几个参数。-an 是不要音频,-f null - 是把结果丢掉只跑一遍滤镜,2>&1 是把 ffmpeg 写在错误通道里的日志合并进普通输出,这样后面的 grep 才抓得到那几行数字。-c:v libvpx-vp9 里的 libvpx-vp9 是 VP9 的一个外部编解码库,FFmpeg 可以调用它来读透明数据。我的理解是默认路径用的是 FFmpeg 自带的 VP9 解码器,两者在 7.1 这个构建上对 alpha 的处理不同。更细的原因我没有去查。
最后说一个容易搞混的点。命令里 -c:v 出现的位置决定它管什么。放在 -i 前面管输入,也就是用什么解码器去读。放在 -i 后面管输出,也就是用什么编码器去写。同一个参数名放在两个位置是两件事,第七章会用实测说明放错的后果。
扫满 240 帧才算确认
抽一帧能说明问题,但不能当作整条视频都没问题的证据。一条透明视频完全可能前半段有透明而后半段没有。我把 240 帧都扫了一遍:取出每一帧 alpha 的均值,找最小值和最大值,再数有多少帧的均值恰好等于 255。
swift
$ scan() {
ffmpeg -hide_banner "$@" -i clip.webm -vf "format=rgba,alphaextract,signalstats,metadata=print:key=lavfi.signalstats.YAVG" -an -f null - 2>&1 \
| grep -o 'YAVG=.*' | cut -d= -f2 \
| awk 'NR==1{lo=hi=$1} {if($1<lo)lo=$1; if($1>hi)hi=$1; n++; if($1==255)full++} END{print n" 帧, 最小 "lo", 最大 "hi", 均值=255 的帧数 "full+0}'
}
$ scan
$ scan -c:v libvpx-vp9
240 帧, 最小 255, 最大 255, 均值=255 的帧数 240
240 帧, 最小 42.9292, 最大 51.5385, 均值=255 的帧数 0
代码说明:先定义一个叫 scan 的小函数,它把传进来的参数原样放在 -i 前面,再往下读 alpha。scan 不带参数走默认解码,scan -c:v libvpx-vp9 带上解码器,两次调用的滤镜链完全一样。函数里 grep -o 'YAVG=.*' 和 cut -d= -f2 负责把日志里每一帧的均值单独捞出来,每帧一行,awk 一边读一边记最小值和最大值,顺手数均值恰好是 255 的帧。输出的两行对应两次调用,数字是真实运行结果。
我写成函数而不是把解码器塞进一个变量再循环,是因为 zsh 不会把变量按空格拆成两个参数,macOS 上默认就是 zsh。这段在 bash 和 zsh 里我都跑过,输出一致。另外我用 format=yuva420p 的写法把指定解码那一轮单独跑过一遍,最小值 42.929、最大值 51.538,和这里对得上。
默认解码那一行最干脆:240 帧里每一帧的均值都是 255。指定解码器那一行均值最小 42.9292、最大 51.5385,没有一帧等于 255。这条视频从头到尾都带着真实的透明,只是每一帧 alpha 的均值随人物动作在 43 到 52 之间浮动。所有帧的均值再取个平均是 46.733。
这个检查动作的核心判据只有一条。同一个文件用指定解码器读出来的 alpha 只要出现了小于 255 的值,这个文件就带透明。默认解码读出来全是 255 不能说明文件不带透明,只能说明默认路径没读。两个方向的结论并不对称,写成表格大概是这样:
| 读法 | 读到全是 255 | 读到小于 255 的值 |
|---|---|---|
| 默认解码 | 什么也说明不了 | 说明文件带透明,且默认路径读得到 |
| 指定 libvpx-vp9 | 文件很可能确实不带透明 | 文件确实带透明 |
这个表是我自己的归纳。默认解码要是读到了小于 255 的值,意味着这个构建在默认路径下也读得到,我手上这个构建没有出现过这种情况。
三、alpha_mode 标签为什么靠不住
标签可以残留也可以丢失
很多人的第一反应是既然 ffmpeg -i 就能看到 alpha_mode : 1,直接以标签为准不就完了。这个想法很自然,我就专门做了两个反例去验证。结论是标签两头都不可靠。
第一个反例叫 noalpha.webm,是我自己构造的。做法是把透明 WebM 用指定解码器解出来再按 yuv420p 重新编码成 WebM,这样画面里就没有 alpha 了。用 ffmpeg -i 去看它,标签依然是 ALPHA_MODE : 1。原因是 FFmpeg 重新编码时会把输入流的元数据原样带到输出,标签跟着过去了,数据没有。这是用 FFmpeg 重新编码带标签文件时的副作用,我构造反例时复现了出来。它不是图映导出的问题也不是某个工具的缺陷。
第二个反例叫 noflag.webm,做法反过来。先用 FFmpeg 的 libvpx-vp9 以 yuva420p 编码 2 秒带透明的视频,再用 -c copy 配合 -metadata:s:v:0 alpha_mode= 把标签清掉。结果 ffmpeg -i 看不到任何 alpha 标签,但指定 libvpx-vp9 解码后第 30 帧的 alpha 最小 0 最大 255,均值 45.48,透明数据完完整整地在。
两个反例放在一起结论就出来了:标签可以残留,有标签没数据,也可以丢失,有数据没标签。只看标签的话,第一个会误报,第二个会漏报。一个文件被重新封装过几次、经过几个工具之后,标签和数据还对不对得上,没有任何保证。
要数视频块里的 BlockAdditions
既然标签不可靠,可靠的东西在哪里。我的办法是直接数视频块里有没有挂附加数据,也就是 BlockAdditions。上一章推测过透明数据是每个视频块旁边挂的一份附加数据,那就去数这份附加数据有没有、有多少。
先看四个文件的对照,数字来自我自己写的手解脚本:
| 文件 | ffmpeg -i 看到的标签 | AlphaMode | BlockAdditions 数量 | 附加数据字节 |
|---|---|---|---|---|
| 图映导出的透明 WebM | alpha_mode : 1 | 1 | 240 | 1,036,225 |
| FFmpeg 自己编码的 2 秒带透明 WebM | ALPHA_MODE : 1 | 1 | 60 | 248,054 |
| 反例一,有标签没数据 | ALPHA_MODE : 1 | 1 | 没有 | 没有 |
| 反例二,有数据没标签 | 没有标签 | 没有 | 60 | 248,054 |
四行连起来看,后两行的标签和 BlockAdditions 是反着的。唯一靠得住的是 BlockAdditions。有它,指定解码器就读得到 alpha,没有它就读不到。第二行的附加数据 248,054 字节占 533,098 字节文件的约 46.5%,和第一行的约 43% 在同一个量级。这说明 alpha 数据不是一个小零头,而是视频体积的一大块。
光数个数还不够直观,我又把另外三个文件各读了一遍 alpha,取第 30 帧(这两个 2 秒的文件一共 60 帧),默认解码和指定 libvpx-vp9 解码各一次,滤镜链和第二章那一对一样,只把帧号改成 30。读到的 alpha 最小、均值、最大如下。
| 文件 | 默认解码 | 指定 libvpx-vp9 解码 |
|---|---|---|
| FFmpeg 自己编码的 2 秒带透明 WebM | 255 / 255 / 255 | 0 / 45.4818 / 255 |
| 反例一,有标签没数据 | 255 / 255 / 255 | 255 / 255 / 255 |
| 反例二,有数据没标签 | 255 / 255 / 255 | 0 / 45.4818 / 255 |
这张表要看两件事。第一,默认解码那一栏三个文件全是 255,它分不出谁有透明谁没有,所以前面说默认解码读到 255 什么也说明不了。第二,指定解码器那一栏把三个文件分开了:反例一读到的还是全 255,和它的 BlockAdditions 是 0 对得上,反例二读到真实的 alpha,和它的 60 个 BlockAdditions 对得上。反例二的均值和 FFmpeg 自己编码的那一个完全相同,因为它本来就是后者去掉标签复制出来的,也能看出去掉标签没有动到视频块里的透明数据。这几个读数同样只属于这几个文件,只测了第 30 帧。
这里要老实交代一件事。我没有测 ffprobe 的 show_packets 路线,因为本机没装 ffprobe 也没有 mkvinfo。有这些工具的话也许不用自己手解。我是在没有工具的情况下被逼着去读 EBML 结构的。
手解脚本只做两件事
WebM 是 Matroska 的子集,用的是 EBML 这种长度加内容的嵌套格式。脚本要做的事其实很少:从头往后读,每个元素读出编号和长度,遇到几个容器元素就钻进去继续读。遇到 AlphaMode 就记下它的值,遇到 BlockAdditions 就计数加一。下面是我写的版本,只用标准库。
python
# 用法:python3 scan_alpha.py 文件.webm
# 只做两件事:找 AlphaMode 标签,数视频块里挂了多少个 BlockAdditions
import sys
def read_vint(buf, pos, keep_marker):
first = buf[pos]
width = 1
while width <= 8 and not first & (0x80 >> (width - 1)):
width += 1
num = first if keep_marker else first & ((0x80 >> (width - 1)) - 1)
for k in range(1, width):
num = (num << 8) | buf[pos + k]
return num, width
ALPHA_MODE, BLOCK_ADDITIONS, BLOCK_GROUP = 0x53C0, 0x75A1, 0xA0
# 只有这些容器元素需要往里钻
INSIDE = {0x18538067, 0x1654AE6B, 0xAE, 0xE0, 0x1F43B675, BLOCK_GROUP}
def scan(buf, start, stop, found):
pos = start
while pos < stop:
try:
eid, w1 = read_vint(buf, pos, True)
size, w2 = read_vint(buf, pos + w1, False)
except IndexError:
break
body = pos + w1 + w2
end = min(stop, body + size)
if eid == ALPHA_MODE:
found["alpha_mode"] = buf[body]
elif eid == BLOCK_ADDITIONS:
found["block_additions"] += 1
if eid in INSIDE:
scan(buf, body, end, found)
pos = end
data = open(sys.argv[1], "rb").read()
result = {"alpha_mode": None, "block_additions": 0}
scan(data, 0, len(data), result)
print(sys.argv[1], result)
ruby
$ python3 scan_alpha.py clip.webm
clip.webm {'alpha_mode': 1, 'block_additions': 240}
$ python3 scan_alpha.py noalpha.webm
noalpha.webm {'alpha_mode': 1, 'block_additions': 0}
$ python3 scan_alpha.py noflag.webm
noflag.webm {'alpha_mode': None, 'block_additions': 60}
代码说明:read_vint 读 EBML 的变长整数。元素编号要保留最高位的标记而长度要去掉标记,所以才有 keep_marker 这个开关。INSIDE 里列的是需要钻进去的容器。包括 Segment、Cluster、Tracks、TrackEntry、Video 和 BlockGroup,透明数据挂在 BlockGroup 下面。三个文件跑出来的结果和上面的表一致:clip.webm 有标签也有 240 个附加块。noalpha.webm 有标签但附加块是 0,noflag.webm 没标签却有 60 个附加块。这个脚本只数个数而没有校验附加块里是不是合法的 VP9 数据,也没有解出透明值。数到附加块只能说明很可能有,最后还得用上一章的指定解码器命令读出真实 alpha 才算数。
脚本有个限制要说清楚。它是按我手上这几个文件的结构写的,遇到 EBML 里长度字段全 1 的未知长度元素只做了最简单的处理。换成别的封装器输出的文件有可能数不准。它适合做一次快速判断,不适合放进什么正式流程。
四、转剪辑中间格式时透明在哪一步丢掉
默认解码转出的 ProRes 照样能播
透明 WebM 拿去剪辑。常见的下一步是转成剪辑软件更爱吃的中间格式,比如 ProRes 4444。也有人用 QuickTime Animation,也有人导出一串 PNG。这一步里透明丢掉的方式最隐蔽:文件既不会报错也不会变得残缺。
最容易走的一条路是这样的。用一条看上去完全正常的命令把 WebM 转成 ProRes 4444,不指定解码器。命令跑完文件生成了,体积 41,105,728 字节而且能播放。画面也对,但这个文件整条视频是不透明的。QuickTime Animation 同样,默认解码转出来是 85,557,735 字节,整条不透明。
把这件事摊开看,它是一个典型的上游丢了、下游照常生成。解码这一步没有读到透明,下游编码器收到的就是一路没有 alpha 的画面。它再忠实地编码 yuva444p10le 也只是把 255 编码进去。ProRes 4444 这个格式本来就支持 alpha。这一步没有任何环节出错,只是源头给的 alpha 已经是满格不透明。我还要补一句:这和 ProRes 好不好、剪辑软件认不认没有关系,纯粹是转的时候漏了一个参数。
看参数分不出有没有透明
接着一个疑问是转出来的 mov 文件能不能靠 ffmpeg -i 一眼看出有没有透明。我试了,看不出来。默认解码转的那份和指定解码器转的那份,ffmpeg -i 看到的视频流信息用的是同一个像素格式。
| 文件 | Stream 行里的格式 | 码率 | 字节 |
|---|---|---|---|
| 默认解码转出的 ProRes | prores (4444),yuva444p12le | 41103 kb/s | 41,105,728 |
| 指定解码器转出的 ProRes | prores (4444),yuva444p12le | 62032 kb/s | 62,034,554 |
两行都写着 yuva444p12le,带着字母 a,看上去都是有 alpha 的。区别只剩码率和体积,有透明的那份大了约一半。但体积不是判据,不同内容、不同参数下体积本来就会变。
那就老老实实去读 alpha。我把两个 ProRes 文件的第 60 帧 alpha 取出来:默认解码那份的最小值、均值、最大值都是 3773,是一个常数;指定解码器那份最小 256、均值 899.108、最大 3763。再扫满 240 帧,默认解码那份每帧均值都是 3773.0。指定解码器那份在 846.399 到 964.798 之间浮动。这里的数字不是 0 到 255,因为 alpha 是 12 位刻度。这些数值我没有换算也不拿它跟 255 对照,只用来判断一件事:是不是一个常数。常数就是没有透明,在变就是有。
指定解码器后三种格式都保住透明
方案很简单,在输入文件前加一个 -c:v libvpx-vp9。下面是转 ProRes 4444 和 QuickTime Animation 的命令。
less
$ ffmpeg -hide_banner -c:v libvpx-vp9 -i clip.webm -an -c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le out.mov
Stream #0:0(eng): Video: prores (4444) (ap4h / 0x68347061), yuva444p10le(tv, smpte170m, progressive), 540x960 [SAR 1:1 DAR 9:16], q=2-31, 200 kb/s, 30 fps, 15360 tbn (default)
frame= 240 fps=0.0 q=-0.0 Lsize= 60581KiB time=00:00:08.00 bitrate=62034.6kbits/s speed=8.18x
(out.mov 62,034,554 字节)
$ ffmpeg -hide_banner -c:v libvpx-vp9 -i clip.webm -an -c:v qtrle -pix_fmt argb out_qtrle.mov
frame= 240 fps=0.0 q=-0.0 Lsize= 90016KiB time=00:00:08.00 bitrate=92176.1kbits/s speed=12.2x
(out_qtrle.mov 92,176,076 字节)
代码说明:两条命令都先指定 libvpx-vp9 解码,再用 -an 去掉音频只留视频。第一条用 prores_ks 编码器,-profile:v 4444 选 4444 档。-pix_fmt yuva444p10le 保留 alpha。第二条用 qtrle,像素格式是 argb。ffmpeg 自己的末行按 KiB 打印,括号里的实际字节是我在命令外用 ls 补上的。speed= 这类数值只是一次运行的现场读数而不是性能结论,我不引用。
转完以后要回读。方法是把转出来的文件再解出第 60 帧。看 alpha 的最小值、最大值和均值,并和指定解码器读原文件得到的 0、255、46.762 比较。结果三种格式的回读完全一致:ProRes 4444、QuickTime Animation、PNG 序列(240 个文件)的第 60 帧 alpha 都是 0、255、46.762。这个相同的数字让我比较放心。格式本身没有吃掉透明,只要源头读对了。
回读 ProRes 用的命令是 ffmpeg -hide_banner -i out.mov -vf "select=eq(n\,60),alphaextract,signalstats,metadata=print" -an -f null -,没有写 -c:v libvpx-vp9,因为这时候读的是 mov 里的 ProRes,不是 VP9。我的理解是 ProRes 4444 的 alpha 就放在画面平面里,默认解码器读得到,这是从回读数值推出来的,我没有查资料确认。上一节那两份 ProRes 读到的 3773,以及 256、899.108、3763,用的就是这条命令。要提醒一句:回读 ProRes 不用指定解码器,不等于读 WebM 也不用,这是两件事,别混在一起。
五、三种中间格式的体积差多少
体积约是透明 WebM 的 26 到 38 倍
保住透明是有代价的:代价就是体积。下面这张图把所有格式放进了一张表,包括没有保住的那几种。

这张图要看什么:表一共七行,左列是格式,中间列是文件字节,右列是透明保留与否。前五行都有透明,其中第五行再编码那一条写的是「是(边缘有损耗)」。最后两行底色是淡红,写的是「否」,对应默认解码转出的 ProRes 4444 和 H.264 MP4。图下方一行小字说明体积是实际字节,输入约 8.03 秒、540×960,透明保留按第 60 帧回读的 alpha 判断。
按字节算,ProRes 4444 的 62,034,554 字节是原 WebM 的约 26 倍。QuickTime Animation 的 92,176,076 字节约是 38.5 倍,PNG 序列 240 个文件加起来 63,857,616 字节约是 26.7 倍。输入的 WebM 含大约 0.3 MB 的音频而中间格式我都去掉了音频,倍数算得稍微保守一点。这几个倍数只对这一段素材成立。
三种格式里我自己的取舍很直白。PNG 序列体积和 ProRes 差不多,但它是 240 个文件。管理起来麻烦,适合只拿几帧来看或者喂给特定的软件。QuickTime Animation 最大,92 MB 放 8 秒视频不是个好选择。ProRes 4444 体积居中又是一个文件,在剪辑圈里是常见的中间格式,我自己会优先选它。这只是取舍。我没有在任何一款剪辑软件里实际导入验证过,不拿它当推荐。
挑格式要看体积文件数和用途
三种格式在透明保留上打平,选哪个就只能看别的维度了。我手上能用来比较的只有三项实测:体积、文件个数、透明回读是否一致。其他维度比如剪辑软件里的实际手感我没有测过,下面只对这三项下判断。
体积上 PNG 序列和 ProRes 4444 差不多,QuickTime Animation 最大。文件个数上 PNG 序列是 240 个,另外两种各是一个。一个 8 秒的素材拆成 240 个文件还算勉强。换成一分钟的视频就是 1800 个,传输和整理的负担会跟着帧数涨。这条推算很简单,我没有实际导出过一分钟的素材,这只是算术,不是实测。
用途上的区别我只能做一个推断。PNG 序列的每一帧是一张独立的图,需要挑几帧出来看效果或者单独修某一帧的时候比较方便。ProRes 4444 和 QuickTime Animation 是一个完整的视频文件,拖进时间线就是一条片段。这两句话是格式本身的性质,不是我在某个软件里验证出来的体验。
我的取舍可以写成一个判断顺序。要把整条视频交给下一个环节就先考虑 ProRes 4444,要逐帧处理或者下游明确要图片序列就用 PNG 序列。QuickTime Animation 在这条素材上体积最大,我没有遇到非它不可的理由便放在最后。这个顺序依据的只是这一段素材上的三项数字。下游有明确格式要求的时候以对方的要求为准。
再编码一次透明还在但略有损耗
还有一个对照。把透明 WebM 解出来,再用 libvpx-vp9 以 crf 30 编码成新的带透明 WebM。体积是 2,177,103 字节,比原文件还小一点。回读第 60 帧,alpha 均值是 46.761。比原文件的 46.762 低 0.001。不透明占比从 15.83% 降到 15.58%,完全透明占比从 78.91% 降到 78.67%。
这说明透明在再编码之后还在,但 alpha 本身也被有损压缩了。边缘一圈半透明的像素会有微小变化,数字上看差得不多。不过素材边缘本来就很精细的话,比如头发丝,多编码一次就多一次损耗,肉眼未必看得出而数字上看得出。我没有对这个损耗做肉眼对比,所以只写数字,不写肉眼有没有差别。
H.264 本来就没有透明层
最后一行是 H.264 MP4,yuv420p。crf 18,体积 1,440,811 字节是最小的一个。它回读的 alpha 是 255、255、255.0,全不透明。这行放进来是为了说明另一种丢透明:不是读丢了,而是格式本身没有 alpha 这一项。H.264 的标准视频不带透明层。转成它透明就没了,和解码器没有关系。
看到透明没了的时候要分清楚是哪一种。一种是文件里有透明但没读出来:换解码器就能找回来。另一种是格式不支持透明:转出去以后不可逆。要回到上游文件重来。我把 H.264 当作下限放进对照里。它体积小不代表这个方案更好。换成纯色或者图片背景导出时 MP4 恰好是好选择,但那是另一件事了。
六、拿到透明 WebM 该按什么顺序检查
把前面几章合起来,我给自己排了一个检查顺序。每一步都对应一条前面跑过的命令,不超出实测范围。这个顺序是实测之后才排出来的,没有拿它去验证其他素材。别当成通用流程,它只是拿一个文件走过一遍的路径。
第一步用 ffmpeg -i 看文件信息,记下视频流是不是 VP9。Metadata 里有没有 alpha_mode 标签。有标签就当线索,没有标签也不下结论。顺手看一眼标签的大小写,ffmpeg 自己写的文件里是大写的 ALPHA_MODE,浏览器端封装器写的是小写的 alpha_mode,两种都要认。
第二步取一帧,分别用默认解码和指定 libvpx-vp9 解码并对比 alpha 的最小值、均值和最大值。这一步用的就是第二章的那一对命令。指定解码器读到小于 255 的值就说明文件带透明,可以进入下一步。两种方式都是全 255 的话再去数 BlockAdditions,别急着判定没有。
第三步扫满整条视频,数均值恰好是 255 的帧有几个。这一步用第二章的那个循环。我这条素材里没有一帧均值是 255。别的素材里如果零星有几帧是 255 也不必慌:要看的是整体有没有变化。
第四步在需要更确定的时候数 BlockAdditions。可以用第三章的手解脚本。手里有 ffprobe 或 mkvinfo 的话也可以用它们,我没试过那条路。BlockAdditions 的数量和视频帧数大致对得上,说明每一帧都挂着透明数据。
第五步是转中间格式的时候。解码一定写 -c:v libvpx-vp9 并且放在输入前面。转完再回读一帧,用 alphaextract 加 signalstats 看是不是常数。是常数就说明这个文件已经不透明了。回头重转,不要把它交给下一个环节。
这五步里最值钱的是第五步。前四步判断的是源文件,第五步判断的是你交出去的那份文件。丢透明发生在转的时候,检查也要放在转完之后。
把读数和后续动作对起来,我手上这几个文件大致是下面几种情形。指定解码器读到小于 255 的值,BlockAdditions 也有,比如前面那个导出的透明 WebM,说明透明数据是真的,转格式时写上 -c:v libvpx-vp9 就行。指定解码器读到的仍是全 255,BlockAdditions 是 0,哪怕标签还在,比如我构造的反例一,说明这个文件里已经没有透明数据了,转格式救不回来,要回到上游重新导出。指定解码器读得到 alpha 但标签没了,比如反例二,文件照样可用,只是别指望靠标签去筛它。最后一种是转完回读发现 alpha 是常数,那就是转的时候漏了解码器,回头重转。这四种我都在本机见过,没有见过的组合我不写,比如默认解码居然读到了透明。
七、测量这件事自己有哪些坑
这一章单独讲测量方法自身的坑。我查透明的时候有几次是命令本身给了误导的结果,下面列的是实际遇到、能复现的几条。
滤镜链里没有透明平面会直接报错
默认解码路径下如果不先 format=rgba 就直接 alphaextract,ffmpeg 会报 Could not open encoder ... Invalid argument。原因是解出来的帧里没有 alpha 平面可抽。这个报错很容易被理解成命令写错了,实际上它在告诉你这路视频本来就没有透明。我在默认解码那条命令里加了 format=rgba,滤镜链才能跑通。代价是读到的 255 是被补上去的,不是原文件里的值。对比两种解码时两边的滤镜链并不完全一样,这一点我在代码说明里写了,读数的时候别忘了。
解码器参数放错位置读到的是常数
-c:v libvpx-vp9 放在 -i 前面才是用这个解码器读输入。放在 -i 后面是用这个编码器写输出,对读输入不起作用。我专门试了一次:把它写在 -i 后面取第 60 帧,滤镜是 format=yuva420p,alphaextract,signalstats。结果最小值、均值、最大值三个数都是 235。三个数相同说明是常数而不是真实的 alpha。至于为什么是 235 而不是 255,我没有深究,大概是满不透明在限幅范围里的写法,这只是推测没有核实。我只确认了一件事:位置写错了读到的就是一个没有信息的常数。
这个坑特别阴:命令能跑通而且输出看起来也像有数。如果你刚好只看了均值,235 和 255 看上去差不多而可能会以为有一点透明。三个数相等才是关键信号。
刻度不同的数字不能直接比
前面 ProRes 的读数是 3773、256、3763 这样的数,和 0 到 255 的刻度对不上。原因是 ProRes 4444 解出来是 12 位的像素格式,alpha 也跟着是 12 位。我没有换算,也没有拿它去和原文件的 46.762 比。刻度不一样,直接比会得出错误的结论。要比的话要么统一转成 rgba 再取 alpha,要么只看是不是常数。我这一步用的是后者。
另一个类似的刻度问题是体积。ffmpeg 自己打印的是 KiB(1024 字节一个单位),ls 给的是字节。我在文中全部用字节,MB 一律按 1,000,000 字节。界面里显示的 MB 是 1,048,576 字节。拿界面里的数字去对照本文会有大约 4.9% 的差,别以为是我算错了。
标签大小写会让人漏查
ffmpeg 自己写出来的 WebM 标签是大写的 ALPHA_MODE,浏览器端封装器导出的文件里是小写的 alpha_mode。如果你写脚本去 grep 标签只写了一种,另一种就会被漏掉。我构造 noalpha 和那个 2 秒的带透明文件时见到的都是大写,导出的原文件是小写。这是一个很小的坑但在判断有没有标签的环节最容易凭空多出误报或漏报。
这几条坑有个共同点:命令都能跑通,输出看上去也像那么回事。我自己的习惯是每次先看两样东西。一样是三个数是不是相等,相等就是常数,别信。另一样是指定解码器之后,Stream 行里的像素格式有没有变成带 a 的 yuva420p。不过后一样只能说明这一次是怎么读的,说明不了文件本身带不带透明,前面已经讲过,别拿它当结论。
八、还有哪些问题没有查清
这篇里有几件事我想查而没有查成,放在最后是免得你以为它们已经有结论。
最大的一个是剪辑软件那一环。我想知道透明 WebM 交给剪辑软件会发生什么,能不能读到 alpha,可是没有找到可靠的官方说明,本机也没有装这类软件,没法自己试。所以交接的时候,转成 ProRes 4444 或 PNG 序列是我觉得更稳的做法,这个说法的依据只是本机 FFmpeg 的回读实测,不是哪款软件支持。
第二个是 ffprobe 的路线。如果你有 ffprobe,查 packet 的 side_data 也许能直接看到 alpha 附加数据而不用手解。我没有测也不知道它能不能区分我构造的那两个反例。
第三个是版本。FFmpeg 7.1 默认解码不读 alpha 是这个构建上的事实,更新的构建、别的发行版有没有改默认行为我不知道。如果你的版本读得到那是好事。转格式前还是建议回读一次,成本只是一条命令。
第四个是素材本身。这段视频是绿布不太均匀的真人口播,画面里有两个人。我没有找到更合适的第二条素材,所有数字都只属于这一条。换成人物更少、边缘更简单的素材,半透明像素的占比和体积倍数都会变。
第五个是 alpha 的存放方式。透明视频的 alpha 有直通和预乘两种常见做法。我没有验证这条 WebM 用的是哪一种,也没有验证转出的 ProRes 保持了哪一种。回读数值一致只能说明数值没有变,说明不了下游软件按哪种方式解释它。素材边缘有半透明发丝的话这一项值得单独核对。
常见问题
为什么默认解码读不到透明却不报错
我的理解是默认路径只读主画面这一路,附加的透明数据没有被送进解码。后面拿到的是一路没有 alpha 的画面,程序没有理由报错。这是从实测表现推出来的,我没有读 FFmpeg 的源码。实测能确认的是:在 7.1 这个构建上默认解码下 alpha 恒为 255。ffmpeg -i 的 Stream 行写 yuv420p,不提示丢了 alpha。
只看 alpha_mode 标签行不行
不行,两个反例已经说明了。标签可以在 FFmpeg 重新编码后残留而数据却没了,也可以被去掉而数据还在。标签是线索不是证据。要判断至少做第二章那一对解码比较,要更确定再数 BlockAdditions。
转成 ProRes 之前一定要先检查吗
建议先做,转完做一次回读也行。检查的成本是一条命令加一次读数。不检查的后果是文件照样生成而整条不透明,直到交给剪辑才发现。我这次实测里默认解码转出的 ProRes 4444 有 41,105,728 字节,比正确转出的小了约三分之一。体积更小并不意味着更好,别被它骗了。
转完回读一帧够不够
看你想防什么。像默认解码漏了解码器这种整条都不透明的问题,读一帧就能发现,第 60 帧读出来是常数,整条也是常数。我只在两份 ProRes 上扫满过 240 帧:默认解码那份每一帧的均值都是 3773.0,指定解码那份在 846.399 到 964.798 之间浮动。QuickTime Animation 和 PNG 序列我只读了第 60 帧,没有扫满,所以它们的结论只到这一帧为止。如果你担心个别帧出问题,成本也不高,把第二章那个 scan 函数里的 -i clip.webm 换成你的 mov 文件,再去掉解码器参数,一遍就能扫完。我这样把那两份 ProRes 扫过:滤镜链里有 format=rgba,读数回到 0 到 255 的刻度,默认解码转出的那份 240 帧全是 255,指定解码转出的那份最小 42.9292、最大 51.5385,和原 WebM 读出来的一致。
导出的这个透明 WebM 有什么特别吗
没有。这个透明 WebM 是图映 ImgIng 的视频抠像导出的,它在电脑浏览器里本地处理,目前是 Beta,手机上不能用。它导出的文件里视频块都带着 BlockAdditions,指定 libvpx-vp9 解码能读到真实 alpha。文中所有丢透明的现象都出在后面的解码和转格式环节,和导出的文件没有关系。两个反例也是我自己用 FFmpeg 构造的。
参考资料
- VOA Learning English《Filming with the green screen》,公有领域,维基共享资源,文件页:commons.wikimedia.org/wiki/File:F...
- FFmpeg 7.1 命令行输出:本机 2026-10-27 单次实测,素材为上述视频的 0 到 8 秒,540×960,30 fps
- 图映视频抠像导出的透明 WebM:2,394,232 字节,240 帧,含 Opus 音频