前几天因为某些需求,使用nV家的ShadowPlay(下称:SP)进行录屏。我录屏一般都是用较高的码率(60Mbps)录制,然后再手动二压一次。这次由于视频比较长而且质量不是很关键,就用NVEnc直接压了遍。
压完之后随便拖了下进度条对比——咦,颜色怎么有点不一样?
左:原始SP录制视频用Pot播放;右:NVEnc压制后用Pot播放
可以明显看到,左边的图红色要暗一些,右边(压制后)则艳丽许多。
根据我的经验,知道这肯定是YUV色彩空间的问题。赶紧来看看metatag:好家伙,SP录出来的视频居然是:
Color range : Limited
Color primaries : BT.601 NTSC
Transfer characteristics : BT.601
Matrix coefficients : BT.601
(注:比较老版本的SP,transfer用的是 BT.470 System M。)
那么看来,似乎应该很简单就是NVEnc转换时没有进行任何处理,生成的视频又没有tag,导致播放器默认按照BT.709播放错误了呗。至于原始视频,虽然是BT.601,但是由于有加正确的tag,那么肯定解码、转换成RGB域之后是正确的。
不过为了谨慎起见,让我们换个播放器来试试。一般而言,如果出现播放异常的情况,我第一个排查的方式就是分别用:MPV、LAV+MadVR(MPC-BE)、LAV+EVR各播放一遍来和PotPlayer的结果对比。即是说,我一般是不信任Pot的结果的“正确性”的。虽然我的PotPlayer在我调教下已经基本可以*正确*播放99%的视频,但是总有无法覆盖的奇怪情况不是。
这么一对比果然就发现了问题:同样播放原始视频的情况下,MPV和LAV+EVR结果一致,但是和LAV或者Pot内置解码器+MadVR的结果不一致。不过这里的不一致和上面情况不一样,更加微小:
左:MPV 右:Pot或LAV+MadVR
这里可以看到,会导致不同的是MadVR,而不是解码器部分。在继续分析为什么不同之前,我们需要知道哪个是“正确”的结果。这里有个很简单的方案:用PS绘制一张红色(BT.601和709矩阵区别在红色最明显)图片(我测试时没用纯红而是用了250,0,0,防止clipping影响对比),然后SP录屏,查看结果用吸管工具取色看哪个对即可。图我懒得贴了,结果就是,用MPV的结果是正确的,MadVR错误。
接下来的过程,我尝试使用ffmpeg的各种色域转换命令对视频”修复“,搜索了不少网上的帖子,花了几个小时也绕了很多弯路,感觉按时间顺序叙述就过于啰嗦,所以我直接写最终结论。
结论1:SP录制的视频确实是BT.601的color matrix,但是并不是BT.601的color primaries。metatag标注错误。
这里首先要复习下我在这个帖里讲过的matrix、primaries、和transfer function的区别(下称M、P、T)。
- Color primaries:指定gamut的范围(三个原色[即“primaries”]的位置,白点位置)。理论上回放时只和校准(显示器)颜色有关,所以绝大部分播放器不会使用这个属性。
- Transfer characteristics:指定gamma ramp。譬如,你可以指定为linear等。几乎所有播放器都无视,直接用那个标准的nonlinear的ramp(见下文)。
- Matrix coefficients:这个是比较关键的也几乎是唯一会被播放器读取并采用的,指定了YUV和RGB的转换矩阵。
然后他们的值的区别:
- P:BT.601 NTSC=SMPTE 170M,不等于BT.601 PAL=BT.470 B/G,不等于BT.709,标错理论上会导致显示错误(下述)
- T:所有常见标准的 transfer function 基本都一致,所以即使标错也影响不大
- M:BT.601=BT.470 B/G (PAL)=SMPTE 170M但是同样不等于 BT.709,标错几乎一定会导致显示错误
SP录制出来的视频,确实是用了BT.601的矩阵(M),但是实际上用的依然是BT.709的P——这里可以通过强行修改metatag之后再播放来验证。至于T,由于所有的标准其实都一样,所以标成601还是709不重要。
结论2:一般播放器会无视P,所以metatag写成什么都无所谓;但是MadVR开启calibration之后会对P进行转换。
如上所述,由于P的特性导致在屏幕没有校准的情况下是无法去适配的,所以一般播放器都完全无视了这个metatag。这也是为什么上述SP录制出来的视频用MPV或者LAV+EVR播放显示“正确”。但是MadVR这边,虽然我的屏幕并没有校准,但是为了能让BT.2020的HDR内容tone-mapping到感知上比较合理的SDR颜色,我“被迫”开启了MadVR的“我的屏幕已经calibrated“的选项并且把校准目标设置为BT.709:
否则,HDR视频的颜色直出,和屎一样:
我不知道为什么MadVR没有单独的tone-mapping HDR content的选项,反正我试了hdr tab下的所有选项,都没有用。MPV其实也需要设置,需要开启vo=gpu(或者直接开启copyback的硬解hwdec=auto-copy,会自动开启vo=gpu)。
这里提供一段BT.2020的视频谁想用可以拿去测试自己的视频播放方案: https://1drv.ms/u/s!Akq11jtCTJYwguclNUTq5MsccSRZ3w
呃,有点跑题了。总之,为了能”正确“显示BT.2020的内容,我开启了这个“我的屏幕已经calibrated”的选项。然而这就导致MadVR会把BT.601的Primaries也转换到BT.709。而SP的视频虽然metatag标注是P=BT.601,但是实际是只有按照P=BT.709来播放才能显示正确的RGB颜色,所以在M转换之后(这个是正确的、应该的)MadVR又进行了一次额外的P转换,导致显示结果错误。
细心点可以看到这里transfer function也被指定了。根据我的测试,如果在发生P转换的时候,这个T选项(尤其是gamma)也是会对结果有影响的。不过,如果如果仅有M转换发生时,这个选项选成什么都没有区别。
要不重新编码修复这个视频,最简单的办法是:
- 用ffmpeg指定P:
ffmpeg -i shadowplay.mp4 -color_primaries bt709 -c copy fixed.mkv
这里注意,要保存成mkv,mp4容器无法覆盖掉视频流的P的metatag。麻烦点也可以先直接转换成MKV,然后用MKVToolNix的header editor编辑。总之,最后出来的MKV用mediainfo查看大概是这样:
Color range : Limited
Color primaries : BT.709
colour_primaries_Original : BT.601 NTSC
Transfer characteristics : BT.601
Matrix coefficients : BT.601
播放是正常的,会认为是BT.709的P,MadVR不会再进行转换,结果和MPV一致了(这里Pot的OSD是显示错误的,大概他直接读取了视频流的tag,没有显示MKV容器覆盖上的,请无视):
以上就是SP视频的Primaries metatag的错误和修复方式。
MadVR转换Primaries的正确性
虽然我们看到了如果开启calibrated,MadVR会对P不为BT.709的视频进行一次转换,但是这个转换结果是否正确呢?
让我们试验一下。还是用250,0,0的颜色来举例——我先用zscale制作一个BT.709的视频,用MPV和MadVR播放都正常显示为250,0,0。然后,我们用ffmpeg的colorspace vf,分别转成bt601-6-625、smpte170m、bt601-6-525:
ffmpeg -i zscale.mp4 -vf colorspace=iall=bt709:all={target_colorspace}:format=yuv420p -sws_flags accurate_rnd zscale_to_{target_colorspace}.mp4
(Where target_colorspace = bt601-6-625, bt601-6-525, smpte170m)
接下来我们分别用Pot和MPV播放生成的视频,然后截图取色,结果如下(前两行其实是一样的):
| New colorspace | mediainfo P | mediainfo T | mediainfo M | MadVR | MPV |
| smpte170m | BT.601 NTSC | BT.601 | BT.601 | 235,37,0 | 243,0,0 |
| bt601-6-525 | BT.601 NTSC | BT.601 | BT.601 | 235,37,0 | 243,0,0 |
| bt601-6-625 | BT.601 PAL | BT.601 | BT.470 System B/G | 249,0,0 | 245,0,0 |
这里辨析一下几个非常迷惑的term,基本上:
NTSC标准系列:BT.601 NTSC = smpte170m = bt601-6-525(转换矩阵在mediainfo里就叫“BT.601”无后缀)
PAL标准系列:BT.601 PAL = BT.470 System B/G = bt601-6-625
有些细节问题(比如这俩的M其实是一样的;又比如T在FFMPEG里SMPTE-170M, BT.601-6 625 or BT.601-6 525都用同一个,bt470bg反而是略有不同)这里按下不表。
其中,MPV的结果就是仅做M转换,不做P转换的数值。而MadVR做了P转换之后,可以看到只有PAL的结果是比较正确的,如果是NTSC,反而比不做转换还要差好远。为什么?鬼知道。这里我不是说MadVR的结果一定是错误,毕竟整个流程可以出错的地方太多了(ffmpeg滤镜可能算法有误,我对整个流程的理解可能有误,等等)。但是总体而言,不同matrix的回放已经很成熟,也一般都很准确;但是我们尽可能要规避回放时的Primaries的转换。
我在前文“用ffmpeg静态图转视频”提到过静态图转视频需要进行一次BT.601到BT.709的转换,这个说法是没错的。但是这个转换仅限于矩阵,P是不需要转换的。所以如果真的想用我提到的“不转换,只添加metatag”的方式,也只需要添加M的tag,而不需要添加P的(添加了反而错了——虽然大部分播放器不会表现出区别)。同理,如果要用colorspace vf,也别用iall和all了,而是用ispace和space(但是这个vf要求你必须把所有的转换都显式写出来才能运行,所以你得用超长的 -vf colorspace=iprimaries=bt709:primaries=bt709:ispace=smpte170m:space=bt709:itrc=bt709:trc=bt709:format=yuv420p)。(这里用 bt601 或者 smpte170m 都可以,一样的。)
转换BT.601 Matrix到BT.709
OK,我们已经讲了如何修复SP录制的视频的P的tag的问题,但是他这个BT.601 NTSC的Matrix,虽然正常的播放器播放不会有啥大问题,但是为了兼容性(而且我们本来就要二压),还是把他转成BT.709吧。我们已经知道了P和T没有转换的必要,所以只需要转换M。
基本上,之前提到过的那些转静态图的命令都能用。唯一需要注意的是某些滤镜,例如scale,会复制旧视频的metatag而不是生成新的BT.709的,需要显式覆盖掉。
# in_color_matrix不用写,默认是auto。需要手动加output的metatag覆盖。
ffmpeg -i bt601.mp4 -vf scale=out_color_matrix=bt709:flags=accurate_rnd+full_chroma_int -colorspace bt709 -color_primaries bt709 -color_trc bt709 scale.mp4
# 因为P的tag错误,所以别用iall,逐个显式声明罢。Output metatag自动加。
ffmpeg -i bt601.mp4 -vf colorspace=iprimaries=bt709:primaries=bt709:ispace=smpte170m:space=bt709:itrc=bt709:trc=bt709:format=yuv420p -sws_flags accurate_rnd+full_chroma_int colorspace.mp4
# Input不能自动识别,需要显式声明。Output metatag自动加。
ffmpeg -i bt601.mp4 -vf colormatrix=smpte170m:bt709,format=yuv420p -sws_flags accurate_rnd+full_chroma_int colormatrix.mp4
# 还是推荐用zscale了,精度高。Input自动识别,output metatag自动加
ffmpeg -i bt601.mp4 -vf zscale=matrix=709,format=yuv420p zscale.mp4
用NVEnc的话,是
nvencc64 -i input.mp4 --vpp-colorspace matrix=smpte170m:bt709 -o output.mp4 --audio-copy --colormatrix bt709 --colorprim bt709 --transfer bt709 --colorrange limited
NVEnc的输出默认没tag,这里显式加上正确的tag(虽然一般播放器都会guess所以还好)。
参考文献