代码之家  ›  专栏  ›  技术社区  ›  MoDJ

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

  •  2
  • MoDJ  · 技术社区  · 7 年前

    BT.709 spec 很多次,只是不清楚的是,编码的H.264比特流是否应该将任何伽马曲线应用于编码数据?请注意,BT.709规范中特别提到了类似gamma的公式。苹果提供的OpenGL或金属着色器示例从CoreVideo提供的缓冲区读取YUV数据,不进行任何类型的gamma调整。YUV值被读取和处理,就像它们是简单的线性值一样。我还检查了ffmpeg的源代码,发现在BT.709缩放步骤之后没有应用gamma调整。我那么 created a test video

    关于这个话题的一个很好的背景可以在 Quicktime Gamma Bug . 似乎Quicktime和Adobe编码器的一些历史问题不正确地进行了不同的gamma调整,结果使视频流在不同的播放器上看起来很糟糕。这确实令人困惑,因为如果你与 sRGB ,它清楚地指示了如何应用gamma编码,然后对其进行解码以在sRGB和线性之间转换。如果在创建h.264数据流时矩阵步骤后未应用伽马调整,为什么BT.709会对同一类伽马调整曲线进行如此详细的说明?h.264流中的所有颜色步是否都要编码为直线(gamma 1.0)值?

    如果特定的示例输入会使事情更清楚,我附加了3个颜色栏图像,不同颜色的精确值可以在图像编辑器中显示这些图像文件。

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

    sRGB colorspace

    linear RGB colorspace

    此第三个图像已通过Rec709-elle-V4-Rec709.icc从转换为REC.709配置文件级别 elles_icc_profiles

    BT.709 colorspace ICC

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

    更新:

    根据反馈,我更新了基于C的实现和Metal shader,并将其作为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;
    }
    

    我相信这个逻辑是正确实现的,但我很难验证结果。当我生成一个包含gamma调整颜色值的.m4v文件(osxcolor\u test\u image\u 24bit\u BT709.m4v)时,结果正如预期的那样。但是我发现了一个类似(bars_709_Frame01.m4v)的测试用例 here 似乎不起作用,因为颜色栏值似乎被编码为线性(无gamma调整)。

    对于SMPTE测试模式,0.75灰度为线性RGB(191 191),该RGB是否应在不进行伽马调整的情况下编码为(Y Cb Cr)(180 128 128)或位流中的值是否应显示为伽马调整的(Y Cb Cr)(206 128 128)?

    (后续行动) 在对这个gamma问题做了进一步的研究之后,很明显苹果在AVFoundation中实际使用的是1.961 gamma函数。使用AvassetWriteInputPixelBufferAdapter编码、使用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;
    }
    
    1 回复  |  直到 7 年前
        1
  •  1
  •   Markus Schumann    7 年前

    您最初的问题是:使用BT.709矩阵的H.264编码视频是否包含任何gamma调整?

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

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

    或者,如果您的位使用Rec.709传输函数进行编码,则编码器不会更改伽马值。

    但是您可以将H.264流中的传输特性指定为元数据。(参考ITU-T H.264(2017年4月)E.1.1 VUI参数语法)。因此,编码流携带周围的颜色空间信息,但不用于编码或解码。

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

    颜色空间由原色、传递函数和矩阵系数组成。