【问题标题】:Does H.264 encoded video with BT.709 matrix include any gamma adjustment?带有 BT.709 矩阵的 H.264 编码视频是否包含任何伽玛调整?
【发布时间】:2019-05-23 12:18:04
【问题描述】:

我已经多次阅读BT.709 spec,但不清楚的是编码的 H.264 比特流是否应该将任何伽马曲线应用于编码数据?请注意在 BT.709 规范中特别提到了类似伽马的公式。 Apple 提供了从 CoreVideo 提供的缓冲区读取 YUV 数据的 OpenGL 或 Metal 着色器示例,它们不进行任何类型的 gamma 调整。正在读取和处理 YUV 值,就好像它们是简单的线性值一样。我还检查了 ffmpeg 的源代码,发现在 BT.709 缩放步骤之后没有应用伽马调整。然后我 created a test video 只有两个线性灰度颜色 5 和 26 对应于 2% 和 10% 级别。当使用 ffmpeg 和 iMovie 转换为 H.264 时,输出 BT.709 值为 (YCbCr) (20 128 128) 和 (38 128 128),这些值与 BT.709 转换矩阵的输出完全匹配,没有任何伽玛调整。

可以在Quicktime Gamma Bug 找到有关此主题的大量背景资料。似乎 Quicktime 和 Adob​​e 编码器的一些历史问题不正确地进行了不同的 gamma 调整,结果导致视频流在不同的播放器上看起来很糟糕。这确实令人困惑,因为如果您与sRGB 进行比较,它清楚地表明如何应用伽马编码,然后对其进行解码以在 sRGB 和线性之间进行转换。如果在创建 h.264 数据流时的矩阵步骤之后没有应用伽马调整,为什么 BT.709 会详细介绍相同类型的伽马调整曲线? h.264 流中的所有颜色步长是否都应编码为直线 (gamma 1.0) 值?

如果具体的示例输入会让事情更清楚,我附上了 3 个颜色条图像,不同颜色的确切值可以使用这些图像文件在图像编辑器中显示。

第一个图像在 sRGB 颜色空间中,并被标记为 sRGB。

第二张图像已转换为线性 RGB 颜色空间,并使用线性 RGB 配置文件进行标记。

这第三张图片已使用来自elles_icc_profiles 的 Rec709-elle-V4-rec709.icc 转换为 REC.709 配置文件级别。这似乎是模拟 BT.709 中描述的“相机”伽马所需要做的。

注意右下角的 sRGB 值 (0x555555) 变成线性 RGB (0x171717) 和 BT.709 伽马编码值变成 (0x464646)。不清楚的是我是否应该将线性 RGB 值传递给 ffmpeg,或者我是否应该传递一个已经 BT.709 伽马编码的值,然后需要在客户端中对其进行解码,然后再进行线性转换矩阵步骤以返回 RGB .

更新:

根据反馈,我更新了基于 C 的实现和 Metal 着色器,并作为 iOS 示例项目 MetalBT709Decoder 上传到 github。

编码一个归一化的线性 RGB 值是这样实现的:

static inline
int BT709_convertLinearRGBToYCbCr(
                            float Rn,
                            float Gn,
                            float Bn,
                            int *YPtr,
                            int *CbPtr,
                            int *CrPtr,
                            int applyGammaMap)
{
  // Gamma adjustment to non-linear value

  if (applyGammaMap) {
    Rn = BT709_linearNormToNonLinear(Rn);
    Gn = BT709_linearNormToNonLinear(Gn);
    Bn = BT709_linearNormToNonLinear(Bn);
  }

  // https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf

  float Ey = (Kr * Rn) + (Kg * Gn) + (Kb * Bn);
  float Eb = (Bn - Ey) / Eb_minus_Ey_Range;
  float Er = (Rn - Ey) / Er_minus_Ey_Range;

  // Quant Y to range [16, 235] (inclusive 219 values)
  // Quant Eb, Er to range [16, 240] (inclusive 224 values, centered at 128)

  float AdjEy = (Ey * (YMax-YMin)) + 16;
  float AdjEb = (Eb * (UVMax-UVMin)) + 128;
  float AdjEr = (Er * (UVMax-UVMin)) + 128;

  *YPtr = (int) round(AdjEy);
  *CbPtr = (int) round(AdjEb);
  *CrPtr = (int) round(AdjEr);

  return 0;
}

从 YCbCr 解码到线性 RGB 的实现如下:

static inline
int BT709_convertYCbCrToLinearRGB(
                             int Y,
                             int Cb,
                             int Cr,
                             float *RPtr,
                             float *GPtr,
                             float *BPtr,
                             int applyGammaMap)
{
  // https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.709_conversion
  // http://www.niwa.nu/2013/05/understanding-yuv-values/

  // Normalize Y to range [0 255]
  //
  // Note that the matrix multiply will adjust
  // this byte normalized range to account for
  // the limited range [16 235]

  float Yn = (Y - 16) * (1.0f / 255.0f);

  // Normalize Cb and CR with zero at 128 and range [0 255]
  // Note that matrix will adjust to limited range [16 240]

  float Cbn = (Cb - 128) * (1.0f / 255.0f);
  float Crn = (Cr - 128) * (1.0f / 255.0f);

  const float YScale = 255.0f / (YMax-YMin);
  const float UVScale = 255.0f / (UVMax-UVMin);

  const
  float BT709Mat[] = {
    YScale,   0.000f,  (UVScale * Er_minus_Ey_Range),
    YScale, (-1.0f * UVScale * Eb_minus_Ey_Range * Kb_over_Kg),  (-1.0f * UVScale * Er_minus_Ey_Range * Kr_over_Kg),
    YScale, (UVScale * Eb_minus_Ey_Range),  0.000f,
  };

  // Matrix multiply operation
  //
  // rgb = BT709Mat * YCbCr

  // Convert input Y, Cb, Cr to normalized float values

  float Rn = (Yn * BT709Mat[0]) + (Cbn * BT709Mat[1]) + (Crn * BT709Mat[2]);
  float Gn = (Yn * BT709Mat[3]) + (Cbn * BT709Mat[4]) + (Crn * BT709Mat[5]);
  float Bn = (Yn * BT709Mat[6]) + (Cbn * BT709Mat[7]) + (Crn * BT709Mat[8]);

  // Saturate normalzied linear (R G B) to range [0.0, 1.0]

  Rn = saturatef(Rn);
  Gn = saturatef(Gn);
  Bn = saturatef(Bn);

  // Gamma adjustment for RGB components after matrix transform

  if (applyGammaMap) {
    Rn = BT709_nonLinearNormToLinear(Rn);
    Gn = BT709_nonLinearNormToLinear(Gn);
    Bn = BT709_nonLinearNormToLinear(Bn);
  }

  *RPtr = Rn;
  *GPtr = Gn;
  *BPtr = Bn;

  return 0;
}

我相信这个逻辑是正确实现的,但我很难验证结果。当我生成一个包含经过伽马调整的颜色值(osxcolor_test_image_24bit_BT709.m4v)的 .m4v 文件时,结果按预期出现。但是我发现here 之类的测试用例(bars_709_Frame01.m4v)似乎不起作用,因为彩条值似乎被编码为线性(没有伽玛调整)。

对于 SMPTE 测试模式,0.75 灰度级是线性 RGB (191 191 191),如果此 RGB 未经 Gamma 调整编码为 (Y Cb Cr) (180 128 128),或者比特流中的值应显示为调整后的伽马 (Y Cb Cr) (206 128 128)?

(跟进) 在对这个 gamma 问题进行了额外的研究之后,很明显 Apple 在 AVFoundation 中实际所做的是使用 1.961 gamma 函数。使用 AVAssetWriterInputPixelBufferAdaptor、使用 vImage 或使用 CoreVideo API 进行编码时就是这种情况。这个分段 gamma 函数定义如下:

#define APPLE_GAMMA_196 (1.960938f)

static inline
float Apple196_nonLinearNormToLinear(float normV) {
  const float xIntercept = 0.05583828f;

  if (normV < xIntercept) {
    normV *= (1.0f / 16.0f);
  } else {
    const float gamma = APPLE_GAMMA_196;
    normV = pow(normV, gamma);
  }

  return normV;
}

static inline
float Apple196_linearNormToNonLinear(float normV) {
  const float yIntercept = 0.00349f;

  if (normV < yIntercept) {
    normV *= 16.0f;
  } else {
    const float gamma = 1.0f / APPLE_GAMMA_196;
    normV = pow(normV, gamma);
  }

  return normV;
}

【问题讨论】:

  • H264 比特流不“应用任何伽马曲线”。编码器会将颜色属性(如果存在)存储为 VUI 标志,并按照呈现的方式对 YUV 数据进行编码。在播放结束时,H264 解码器将读取这些标志(如果存在)并将其传递,因此将光栅数据转换为 RGB 的缩放器可以注意并相应地进行调整。以上是对ffmpeg的参考。
  • 你能再具体一点吗? “相应调整”是什么意思?假设比特流中包含正确的传输属性标志,解码器是否需要在将 YCbCr 数据转换回 RGB 数据之前应用非线性到线性伽马调整?
  • 相机上有 RGB 传感器。基本上,这些值会转换为 YUV。转换是非线性的,加权函数是传递特性或伽马。编码器获取输入 YUV 并对其进行压缩。它将传递函数存储为元数据(如果提供)。解码器生成 YUV,理想情况下接近输入值 - 伽玛不起作用。但是元数据是附加的。将 YUV 转换为 RGB 的缩放器而不是解码器会注意并相应地调整/选择其转换函数。

标签: ios video ffmpeg metal


【解决方案1】:

您最初的问题:带有 BT.709 矩阵的 H.264 编码视频是否包含任何伽玛调整?

编码视频仅包含伽马调整 - 如果您输入编码器伽马调整值。

H.264 编码器不关心传输特性。 所以如果你压缩线性然后解压缩 - 你会得到线性。 所以如果你用 gamma 压缩然后解压缩 - 你会得到 gamma。

或者,如果您的位使用 Rec 编码。 709 传递函数 - 编码器不会改变 gamma。

但您可以将 H.264 流中的传输特性指定为元数据。 (ITU-T H.264 (04/2017) E.1.1 VUI 参数语法建议书)。所以编码流携带了色彩空间信息,但不用于编码或解码。

我会假设 8 位视频总是包含一个非线性传递函数。否则你会相当不明智地使用 8 位。

如果您转换为线性来做效果和合成 - 我建议增加位深度或线性化为浮点数。

色彩空间由原色、传递函数和矩阵系数组成。 伽玛调整在传递函数中编码(而不是在矩阵中)。

【讨论】:

  • 啊,我明白了。我相信我的困惑是因为我发现 Apple 提供的 Quicktime 测试模式有 75% 的灰色表示为 Y = 180,但经过更多思考后,似乎测试条被编码为线性(没有伽马调整)。我使用带有线性 R G B (192 192 192) 的 AVFoundation 手动生成 H.264,并将其编码为经过伽马调整的 Rec.709 Y Cb Cr (211 128 128)。
  • 经过更多测试,这个 Quicktime 测试视频似乎实际上是使用 BT.709 中定义的伽马调整编码的。正确实现的解码器需要在矩阵变换后从解码的 R'、G'、B' 信号中去除这种伽马编码,以便将输出正确转换为线性。
  • 我认为解码器不应该改变色彩空间。我会有一个专用的色彩空间转换器。但如果解码到所需的色彩空间可以在数据上的单个路径中完成,性能会更好。
  • 嗯,在 BT.709 -> sRGB 的情况下,颜色空间转换很简单,因为 709 和 sRGB 共享相同的 RGB 原色。因此,您只需要转换 gamma 编码即可获得正确的输出。我实现的逻辑始终假定编码的 .m4v 文件中的输入位于 BT.709 中,然后在 GPU 上的金属着色器内将伽玛转换为线性。然后,着色器的输出可以由硬件自动转换为 sRGB 伽玛。然后这个 sRGB 颜色表示被发送到显示器。 Metal 源代码 AAPLShaders.metal 包含所有细节。
  • 啊——这听起来很有效。
猜你喜欢
  • 1970-01-01
  • 2012-11-23
  • 2017-05-04
  • 1970-01-01
  • 1970-01-01
  • 2013-06-06
  • 2011-10-30
  • 2011-09-09
  • 2012-12-17
相关资源
最近更新 更多