下面让我疯狂地追逐各种 ffmpeg 选项,但据我所知,所有这些都没有真正记录在案,所以我希望它对那些像我一样对这些相当神秘的行为感到困惑的人有用。
差异是由libswscale的默认参数引起的,该 ffmpeg 组件负责将 YUV 转换为 RGB;特别是,添加full_chroma_int+bitexact+accurate_rnd
标志消除了帧之间的差异:
ffmpeg -i video.mp4 -frames 1 -color_range pc -f rawvideo -pix_fmt rgb24 -sws_flags full_chroma_int+bitexact+accurate_rnd output_good.rgb24
ffmpeg -i video.mp4 -frames 1 -color_range pc -f rawvideo -pix_fmt gbrp -sws_flags full_chroma_int+bitexact+accurate_rnd output_good.gbrp
请注意,各种视频论坛在没有真正提供解释的情况下将这些标志(或其子集)吹捧为“更好”,这并不真正让我满意。它们确实更适合这里的问题,让我们看看如何。
首先,新输出都与gbrp
默认选项的输出一致,这是个好消息!
buffer_rgb24_good = load_buffer_rgb24("output_good.rgb24")
buffer_gbrp_good = load_buffer_gbrp("output_good.gbrp")
diff1 = buffer_rgb24_good.astype(float) - buffer_gbrp.astype(float)
diff2 = buffer_gbrp_good.astype(float) - buffer_gbrp.astype(float)
fig, (ax1, ax2) = plt.subplots(ncols=2, constrained_layout=True, figsize=(8, 2.5))
ax1.imshow(diff1[..., 1], vmin=-5, vmax=+5, cmap="seismic")
ax1.set_title("rgb24 (new) - gbrp (default)")
im = ax2.imshow(diff2[..., 1], vmin=-5, vmax=+5, cmap="seismic")
ax2.set_title("gbrp (new) - gbrp (default)")
plt.colorbar(im, ax=ax2)
plt.show()
ffmpeg 源代码在内部使用以下函数进行转换libswscale/output.c
:
yuv2rgb_full_1_c_template
(和其他变体) for rgb24
withfull_chroma_int
yuv2rgb_1_c_template
(和其他变体)for rgb24
withoutfull_chroma_int
yuv2gbrp_full_X_c
(和其他变体)对于gbrp
,独立于full_chroma_int
一个重要的结论是,full_chroma_int
参数似乎被gbrp
格式忽略了,但没有被忽略,rgb24
并且是统一偏差的主要原因。
请注意,在非rawvideo
输出中,ffmpeg 可以根据所选格式选择支持的像素格式,因此在任何一种情况下都可能在用户不知道的情况下默认获取。
另一个问题是:这些是正确的值吗?换句话说,是否有可能两者都可能以相同的方式产生偏见?使用colour-science Python 包,我们可以使用与 ffmpeg 不同的实现将 YUV 数据转换为 RGB,以获得更多的信心。
Ffmpeg 可以输出原始格式的原始 YUV 帧,只要您知道它们的布局,就可以对其进行解码。
$ ffmpeg -i video.mp4 -frames 1 -f rawvideo -pix_fmt yuv444p output.yuv
...
Output #0, rawvideo, to 'output.yuv':
...
Stream #0:0(und): Video: rawvideo... yuv444p
我们可以用 Python 来阅读:
def load_buffer_yuv444p(path, width=1920, height=1080):
"""Load an yuv444 8-bit raw buffer from a file"""
data = np.frombuffer(open(path, "rb").read(), dtype=np.uint8)
img_yuv444 = np.moveaxis(data.reshape((3, height, width)), 0, 2)
return img_yuv444
buffer_yuv = load_buffer_yuv444p("output.yuv")
然后可以将其转换为RGB:
import colour
rgb_ref = colour.YCbCr_to_RGB(buffer_yuv, colour.WEIGHTS_YCBCR["ITU-R BT.709"], in_bits=8, in_legal=True, in_int=True, out_bits=8, out_legal=False, out_int=True)
...并用作参考:
diff1 = buffer_rgb24_good.astype(float) - rgb_ref.astype(float)
diff2 = buffer_gbrp_good.astype(float) - rgb_ref.astype(float)
diff3 = buffer_rgb24.astype(float) - rgb_ref.astype(float)
diff4 = buffer_gbrp.astype(float) - rgb_ref.astype(float)
fig, axes = plt.subplots(ncols=2, nrows=2, constrained_layout=True, figsize=(8, 5))
im = axes[0, 0].imshow(diff1[..., 1], vmin=-5, vmax=+5, cmap="seismic")
axes[0, 0].set_title("rgb24 (new) - reference")
im = axes[0, 1].imshow(diff2[..., 1], vmin=-5, vmax=+5, cmap="seismic")
axes[0, 1].set_title("gbrp (new) - reference")
im = axes[1, 0].imshow(diff3[..., 1], vmin=-5, vmax=+5, cmap="seismic")
axes[1, 0].set_title("rgb24 (default) - reference")
im = axes[1, 1].imshow(diff4[..., 1], vmin=-5, vmax=+5, cmap="seismic")
axes[1, 1].set_title("gbrp (default) - reference")
plt.show()
由于插值方法和舍入误差略有不同,但没有统一的偏差,因此存在差异,因此两种实现大多同意这一点。
(注意:在此示例中,output.yuv
文件位于yuv444p
,在上述命令行中由 ffmpeg 自动从本机格式转换,yuv420p
而无需进行完整的 RGB 到 YUV 转换。更完整的测试将从单个原始文件执行所有先前的转换YUV 帧而不是常规视频,以更好地隔离差异。)