1

通过使用以下命令行之一,可以将视频流转换为 RGB 缓冲区:

ffmpeg -i video.mp4 -frames 1 -color_range pc -f rawvideo -pix_fmt rgb24 output.rgb24
ffmpeg -i video.mp4 -frames 1 -color_range pc -f rawvideo -pix_fmt gbrp output.gbrp

然后可以读取这些 RGB 缓冲区,例如使用 Python 和 NumPy:

import numpy as np


def load_buffer_gbrp(path, width=1920, height=1080):
    """Load a gbrp 8-bit raw buffer from a file"""
    data = np.frombuffer(open(path, "rb").read(), dtype=np.uint8)
    data_gbrp = data.reshape((3, height, width))
    img_rgb = np.empty((height, width, 3), dtype=np.uint8)
    img_rgb[..., 0] = data_gbrp[2, ...]
    img_rgb[..., 1] = data_gbrp[0, ...]
    img_rgb[..., 2] = data_gbrp[1, ...]
    return img_rgb


def load_buffer_rgb24(path, width=1920, height=1080):
    """Load an rgb24 8-bit raw buffer from a file"""
    data = np.frombuffer(open(path, "rb").read(), dtype=np.uint8)
    img_rgb = data.reshape((height, width, 3))
    return img_rgb


buffer_rgb24 = load_buffer_rgb24("output.rgb24")
buffer_gbrp = load_buffer_gbrp("output.gbrp")

理论上,两个输出应该有相同的 RGB 值(只有内存中的布局应该不同);在现实世界中,情况并非如此:

import matplotlib.pyplot as plt

diff = buffer_rgb24.astype(float) - buffer_gbrp.astype(float)
fig, (ax1, ax2, ax3) = plt.subplots(ncols=3, constrained_layout=True, figsize=(12, 2.5))
ax1.imshow(buffer_rgb24)
ax1.set_title("rgb24")
ax2.imshow(buffer_gbrp)
ax2.set_title("gbrp")
im = ax3.imshow(diff[..., 1], vmin=-5, vmax=+5, cmap="seismic")
ax3.set_title("difference (green channel)")
plt.colorbar(im, ax=ax3)
plt.show()

rgb24和gbrp的区别

转换后的帧与色度二次采样或舍入误差可以解释的差异更大(差异约为 2-3,舍入误差小于 1),更糟糕的是,整个图像似乎有统一的偏差.

为什么会这样,哪些 ffmpeg 参数会影响这种行为?

4

2 回答 2

3

到目前为止分析得很好。让我尝试从 swscale 方面添加一些观点,希望有助于进一步解释您所看到的差异以及它们在技术上的来源。

您看到的差异确实是由不同的舍入引起的。这些差异并不是因为 rgb24/gbrp 根本不同(它们是相同基本数据类型的不同布局),而是因为实现是由不同的人在不同的时间为不同的用例编写的。

yuv420p-to-rgb24(以及其他方式)是非常非常古老的实现,它们来自于 swscale 成为 FFmpeg 的一部分之前。这些实现具有 MMX (!) 优化,并针对 Pentium 机器 (!) 上的最佳转换进行了优化。这是 90 年代中期左右的技术。这里的想法是在 YUV 输出成为事情之前将 JPEG 和 MPEG-1 转换为/从显示器兼容的输出。MMX 优化实际上非常适合他们的时间。

您可以想象速度在这里至关重要(当时,YUV 到 rgb24 的转换很慢,并且是整个显示管道的主要组成部分)。YUV-to-RGB 是一个简单的矩阵乘法(系数取决于确切的 YUV 颜色空间)。但是,UV 平面的分辨率不同于 Y 和 RGB 平面。在简单(非精确)yuv 到 rgb24 的转换中,UV 使用下一个邻域转换进行上采样,因此每个 RGB[x,y] 使用 Y[x,y] 和 UV[x/2,y/2 ] 作为输入,或者换句话说,UV 输入样本对每个输出 RGB 像素重复使用 2x2 次。国旗full_chroma_int“撤消”此优化/快捷方式。这意味着在启动 YUV 到 RGB 转换之前使用实际的缩放转换对色度平面进行上采样,并且这种上采样可以使用诸如双线性双三次或更高级/更昂贵的内核(例如lanczossincspline)之类的过滤器。

bitexact是 FFmpeg 中的通用术语,用于禁用不会生成与 C 函数完全相同的输出的 SIMD 优化。除了说明它的含义之外,我现在将忽略它。

最后, :如果我没记错的话,这里的想法是在矩阵乘法中(与您是否使用色度平面上采样无关),以给定精度accurate_rnd进行浮点整数等效的典型方法(例如使用r = v*coef1 + y15 位系数)是r = y + ((v*coef1 + 0x4000) >> 15). 但是,在 x86 SIMD 中,这需要您使用pmulhrsw仅在 SSSE3 中可用的指令,而不是在 MMX 中可用的指令。此外,这意味着g = u*coef2 + v*coef3 + y您需要pmaddwd使用单独的指令进行舍入/移位。因此,MMX SIMD 改为使用pmulhw(未舍入的版本pmulhrsw),这基本上使它r = y + (v*coef1>>16)(使用 16 位系数)。这在数学上非常接近,但没有那么精确,尤其是对于 G 像素(因为它变成g = (u*coef2 + v * coef3 + 0x8000) >> 16) + yg = (u*coef2>>16) + (v*coef3>>16) + yaccurate_rnd“撤消”此优化/快捷方式。

现在,YUV 到 gbrp。为 H264 RGB 支持添加了 GBR-planar,因为 H264 将 RGB 编码为“只是另一个”YUV 变体,但 G 位于 Y 平面等。您可以想象速度不是问题,MMX 支持也是如此。所以在这里,数学是正确的。事实上,如果我没记错的话,accurate_rnd仅在之后添加,因此 YUV-to-rgb24 可以输出与 YUV-to-gbrp 相同的像素并使两个输出等效,但代价是无法使用合并 swscale 时继承的(旧)MMX 优化进入FFmpeg。默认情况下,这会使用用户配置的缩放内核正确上采样,因为只有当所有 YUV 平面具有相同大小时才会进行平面转换,也就是说,它严格只进行矩阵乘法。这是在 2015 年左右添加的,所以我们谈论的是计算机编程术语中的永恒。

如今,从“不精确”的实现(例如 YUV-to-rgb24)所带来的性能提升与在不精确的舍入和缺乏可配置的色度平面缩放中损失的实际质量相比,被认为是不值得的。这就是为什么大多数人会推荐你使用-sws_flags accurate_rnd+full_chroma_int. 此外,现在有用于“较慢”转换路径的 x86 SIMD(SSSE3 和 AVX2)实现,而在 2010 年左右,这都是纯 C 代码,没有人愿意花时间对其进行优化。我猜这-sws_flags accurate_rnd+full_chroma_int将比“快速” YUV 到 rgb24 转换的性能稍差,因为它分两步而不是一步进行色度上采样和矩阵乘法。但是在现代 x86 硬件上,这样做的性能损失应该是最小的并且可以接受,除非你

希望一切都有意义。

于 2022-01-28T13:43:35.277 回答
0

下面让我疯狂地追逐各种 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 rgb24withfull_chroma_int
  • yuv2rgb_1_c_template(和其他变体)for rgb24withoutfull_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()

rgb24/gbrp 和旧/新标志之间的比较参考

由于插值方法和舍入误差略有不同,但没有统一的偏差,因此存在差异,因此两种实现大多同意这一点。

(注意:在此示例中,output.yuv文件位于yuv444p,在上述命令行中由 ffmpeg 自动从本机格式转换,yuv420p而无需进行完整的 RGB 到 YUV 转换。更完整的测试将从单个原始文件执行所有先前的转换YUV 帧而不是常规视频,以更好地隔离差异。)

于 2022-01-28T12:05:02.440 回答