使用 BT.709 矩阵的 H.264 编码视频是否包含任何伽玛调整?

Does H.264 encoded video with BT.709 matrix include any gamma adjustment?

我读过 BT.709 spec a number of times and the thing that is just not clear is should an encoded H.264 bitstream actually apply any gamma curve to the encoded data? Note the specific mention of a gamma like formula in the BT.709 spec. Apple provided examples of OpenGL or Metal shaders that read YUV data from CoreVideo provided buffers do not do any sort of gamma adjustment. YUV values are being read and processed as though they are simple linear values. I also examined the source code of ffmpeg and found no gamma adjustments being applied after the BT.709 scaling step. I then 只有两种线性灰度颜色 5 和 26 对应于 2% 和 10% 的水平。当使用 ffmpeg 和 iMovie 转换为 H.264 时,输出 BT.709 值为 (YCbCr) (20 128 128) 和 (38 128 128) 并且这些值与没有任何伽马的 BT.709 转换矩阵的输出完全匹配调整。

可以在 Quicktime Gamma Bug. It seems that some historical issues with Quicktime and Adobe encoders were improperly doing different gamma adjustments and the results made video streams look awful on different players. This is really confusing because if you compare to sRGB 找到有关此主题的大量背景资料,它清楚地说明了如何应用伽玛编码,然后对其进行解码以在 sRGB 和线性之间进行转换。如果在创建 h.264 数据流时矩阵步骤后没有应用伽玛调整,为什么 BT.709 会如此详细地介绍同类伽玛调整曲线? h.264 流中的所有颜色步骤是否都应编码为直线 (gamma 1.0) 值?

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

第一张图像在 sRGB 色彩空间中并被标记为 sRGB。

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

第三张图片已使用 Rec709-elle-V4-rec709.icc 从 elles_icc_profiles 转换为 REC.709 配置文件级别 。这似乎是模拟 BT.709.

中描述的 "camera" gamma 所需要做的事情

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

更新:

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

编码归一化线性 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 是否应在没有伽马调整的情况下编码为 (Y Cb Cr) (180 128 128) 或比特流中的值显示为调整后的伽马 (Y Cb Cr) (206 128 128)?

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

#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;
}

您原来的问题:采用 BT.709 矩阵的 H.264 编码视频是否包含伽玛调整?

编码视频仅包含伽玛调整 - 如果您向编码器提供伽玛调整值。

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

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

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

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

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

一种颜色space由原色、传递函数和矩阵系数组成。 伽马调整在传递函数中编码(而不是在矩阵中)。