El video codificado H.264 con matriz BT.709 incluye algún ajuste de gamma?

He leído la BT.709 spec varias veces y lo que no está claro es si un flujo de bits H.264 codificado debería aplicar alguna curva gamma a los datos codificados. Tenga en cuenta la mención específica de una fórmula tipo gamma en la especificación BT.709. Apple proporcionó ejemplos de sombreadores OpenGL o Metal que leen datos YUV de las memorias intermedias proporcionadas por CoreVideo que no hacen ningún tipo de ajuste gamma. Los valores YUV se leen y procesan como si fueran valores lineales simples. También examiné el código fuente de ffmpeg y no encontré ningún ajuste de gamma aplicado después del paso de escala BT.709. Entonces yocrea un video de prueba con solo dos colores lineales en escala de grises 5 y 26 correspondientes a niveles de 2% y 10%. Cuando se convierte a H.264 con ffmpeg e iMovie, los valores de salida BT.709 son (YCbCr) (20 128 128) y (38 128 128) y estos valores coinciden exactamente con la salida de la matriz de conversión BT.709 sin gamma ajuste

Puede encontrar una gran información sobre este tema enQuicktime Gamma Bug. Parece que algunos problemas históricos con los codificadores Quicktime y Adobe estaban haciendo incorrectamente diferentes ajustes de gamma y los resultados hicieron que las transmisiones de video se vieran horribles en diferentes reproductores. Esto es realmente confuso porque si lo comparas con sRGB, indica claramente cómo aplicar una codificación gamma y luego decodificarla para convertir entre sRGB y lineal. ¿Por qué BT.709 entra en tantos detalles sobre el mismo tipo de curva de ajuste gamma si no se aplica ningún ajuste gamma después del paso de matriz al crear un flujo de datos h.264? ¿Todos los pasos de color en una secuencia h.264 deben codificarse como valores lineales rectos (gamma 1.0)?

En caso de que una entrada de ejemplo específica aclare las cosas, adjunto 3 imágenes de barra de color, los valores exactos de diferentes colores se pueden mostrar en un editor de imágenes con estos archivos de imagen.

Esta primera imagen está en el espacio de color sRGB y está etiquetada como sRGB.

Esta segunda imagen se ha convertido al espacio de color RGB lineal y se etiqueta con un perfil RGB lineal.

Esta tercera imagen se ha convertido a niveles de perfil REC.709 con Rec709-elle-V4-rec709.icc deelles_icc_profiles. Esto parece ser lo que uno tendría que hacer para simular la gamma de "cámara" como se describe en BT.709.

Observe cómo el valor sRGB en la esquina inferior derecha (0x555555) se convierte en RGB lineal (0x171717) y el valor codificado gamma BT.709 se convierte en (0x464646). Lo que no está claro es si debería pasar un valor RGB lineal a ffmpeg o si debería pasar un valor codificado con gamma BT.709 que luego debería decodificarse en el cliente antes del paso de la matriz de conversión lineal para volver a RGB .

Actualizar

e acuerdo con los comentarios, he actualizado mi implementación basada en C y el sombreador de metal y los he subido a github como un proyecto de ejemplo de iOS MetalBT709Decoder.

La codificación de un valor RGB lineal normalizado se implementa de la siguiente manera:

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

Decoding de YCbCr a RGB lineal se implementa de la siguiente manera:

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

Creo que esta lógica se implementa correctamente, pero me cuesta mucho validar los resultados. Cuando genero un archivo .m4v que contiene valores de color ajustados por gamma (osxcolor_test_image_24bit_BT709.m4v), el resultado sale como se esperaba. Pero un caso de prueba como (bars_709_Frame01.m4v) que encontréaqu no parece funcionar ya que los valores de la barra de color parecen estar codificados como lineales (sin ajuste de gamma).

Para un patrón de prueba SMPTE, el nivel de granel de 0.75 es RGB lineal (191 191 191), si este RGB se codifica sin ajuste gamma como (Y Cb Cr) (180 128 128) o si el valor en el flujo de bits aparece como gamma ajustado (Y Cb Cr) (206128128)

(seguimiento) Después de hacer una investigación adicional sobre este problema gamma, ha quedado claro que lo que Apple está haciendo realmente en AVFoundation es usar una función gamma 1.961. Este es el caso cuando se codifica con AVAssetWriterInputPixelBufferAdaptor, cuando se usa vImage o con las API CoreVideo. Esta función gamma por partes se define de la siguiente manera:

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

Respuestas a la pregunta(1)

Su respuesta a la pregunta