ColorSpace.hpp
1 #pragma once
2 #include <Video/VideoInterface.hpp>
3 
4 // See softpixel.com/~cwright/programming/colorspace/yuv
5 //
6 // https://github.com/vlc-qt/vlc-qt/blob/master/src/qml/painter/GlPainter.cpp#L48
7 namespace score::gfx
8 {
9 /* OLD code for reference
10 // 601
11 const vec3 R_cf = vec3(1.164383, 0.000000, 1.596027);
12 const vec3 G_cf = vec3(1.164383, -0.391762, -0.812968);
13 const vec3 B_cf = vec3(1.164383, 2.017232, 0.000000);
14 const vec3 offset = vec3(-0.0625, -0.5, -0.5);
15 yuv += offset;
16 r = dot(yuv, R_cf)
17 g = dot(yuv, G_cf)
18 b = dot(yuv, B_cf)
19 
20 // OR (cf wiki https://en.wikipedia.org/wiki/YCbCr)
21 const vec3 offset = vec3(0., -0.5, -0.5);
22 const mat3 coeff = mat3(1.0 , 1.0 , 1.0,
23  0.0 , -0.3455, 1.7790,
24  1.4075, -0.7169, 0.);
25 
26 
27 // https://github.com/vlc-qt/vlc-qt/blob/master/src/qml/painter/GlPainter.cpp#L48
28 const mat4 bt601 = mat4(
29  1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,
30  0.0 , -0.391762290094914, 2.017232142857142, 0.0,
31  1.596026785714286, -0.812967647237771, 0.0 , 0.0,
32  -0.874202217873451, 0.531667823499146, -1.085630789302022, 1.0);
33 const mat4 bt709 = mat4(
34  1.164383561643836 , 1.164383561643836 , 1.164383561643836 , 0.0,
35  0.0 , -0.21324861427373 , 2.112401785714286 , 0.0,
36  1.792741071428571 , -0.532909328559444, 0.0 , 0.0,
37  -0.972945075016308, 0.301482665475862 , -1.133402217873451, 1.0);
38 
39 
40 
41  // yuv += offset;
42 
43  // fragColor = vec4(0.0, 0.0, 0.0, 1.0);
44  // fragColor.r = dot(yuv, R_cf);
45  // fragColor.g = dot(yuv, G_cf);
46  // fragColor.b = dot(yuv, B_cf);
47 
48 OR
49 
50 
51  fragColor = vec4(coeff * (vec3(y,u,v) + offset), 1);
52 
53 OR
54 
55  vec4 tex = texture(u_tex, v_texcoord);
56  float y = tex.r;
57  float u = tex.g - 0.5;
58  float v = tex.a - 0.5;
59 
60  vec4 yuv = vec4(y,u,v, 1.0);
61 
62  // BT402 (wikipedia coeffs)
63  fragColor.r = y + 1.13983 * v;
64  fragColor.g = y - 0.39465 * u - 0.58060 * v;
65  fragColor.b = y + 2.03211 * u;
66  fragColor.a = 1.0;
67 
68 OR
69 
70  // For U0 Y0 V0 Y1 macropixel, lookup Y0 or Y1 based on whether
71  // the original texture x coord is even or odd.
72  vec4 uyvy = texture(u_tex, v_texcoord);
73  float Y;
74  if (fract(floor(v_texcoord.x * renderer.renderSize.x + 0.5) / 2.0) > 0.0)
75  Y = uyvy.a; // odd so choose Y1
76  else
77  Y = uyvy.g; // even so choose Y0
78  float Cb = uyvy.r;
79  float Cr = uyvy.b;
80 
81  vec4 yuv = vec4(Y, Cb, Cr, 1.0);
82 // FIXME 601
83  fragColor = bt709 * yuv;
84 */
85 
86 #define SCORE_GFX_RGB_MATRIX \
87  "mat4(\
88  1., 0., 0., 0.0,\n\
89  0., 1., 0., 0.0,\n\
90  0., 0., 1., 0.0,\n\
91  0., 0., 0., 1.0)\n"
92 
93 #define SCORE_GFX_BT601_MATRIX \
94  "mat4(\
95  1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
96  0.0 , -0.391762290094914, 2.017232142857142, 0.0,\n\
97  1.596026785714286, -0.812967647237771, 0.0 , 0.0,\n\
98  -0.874202217873451, 0.531667823499146, -1.085630789302022, 1.0)\n"
99 
100 #define SCORE_GFX_BT709_MATRIX \
101  "mat4(\n\
102  1.164383561643836 , 1.164383561643836 , 1.164383561643836 , 0.0,\n\
103  0.0 , -0.21324861427373 , 2.112401785714286 , 0.0,\n\
104  1.792741071428571 , -0.532909328559444, 0.0 , 0.0,\n\
105  -0.972945075016308, 0.301482665475862 , -1.133402217873451, 1.0)\n"
106 
107 #define SCORE_GFX_BT2020_TO_709_MATRIX \
108  "mat4(\n\
109  1.660491f, -0.587641f, -0.072850f, 0.000000f,\n\
110  -0.124550f, 1.132900f, -0.008349f, 0.000000f,\n\
111  -0.018151f, -0.100579f, 1.118730f, 0.000000f,\n\
112  0.000000f, 0.000000f, 0.000000f, 1.000000f\n\
113  )\n"
114 
115 #define SCORE_GFX_BT2020_MATRIX SCORE_GFX_BT709_MATRIX
116 #define SCORE_GFX_CONVERT_BT601_TO_RGB \
117  "const mat4 conversion_matrix = " SCORE_GFX_BT601_MATRIX \
118  ";\n" \
119  "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
120 
121 #define SCORE_GFX_CONVERT_BT709_TO_RGB \
122  "const mat4 conversion_matrix = " SCORE_GFX_BT709_MATRIX \
123  ";\n" \
124  "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
125 
126 // https://github.com/cloudveiltech/CloudVeilMessenger/blob/4bb018f58320fb453a40e0add2af061b9f9300ca/TMessagesProj/src/main/res/raw/yuv_hlg2rgb.glsl#L65
127 // https://github.com/Themaister/Granite/blob/7543863d2a101faf45f897d164b72037ae98ff74/video/ffmpeg_decode.cpp#L573
128 
129 // BT2020 code comes from:
130 // https://github.com/google/ExoPlayer/blob/dd430f7053a1a3958deea3ead6a0565150c06bfc/library/effect/src/main/assets/shaders/fragment_shader_transformation_external_yuv_es3.glsl#L25
131 // https://github.com/androidx/media/blob/c35a9d62baec57118ea898e271ac66819399649b/libraries/effect/src/main/java/androidx/media3/effect/DefaultShaderProgram.java#L315
132 static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER = R"_(
133 const int COLOR_TRANSFER_LINEAR = 1;
134 const int COLOR_TRANSFER_GAMMA_2_2 = 10;
135 const int COLOR_TRANSFER_ST2084 = 6;
136 const int COLOR_TRANSFER_HLG = 7;
137 )_";
138 
139 static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER_LIMITED_RANGE = R"_(
140 const mat3 uYuvToRgbColorTransform = mat3(
141  1.1689f, 1.1689f, 1.1689f,
142  0.0000f, -0.1881f, 2.1502f,
143  1.6853f, -0.6530f, 0.0000f
144 );
145 )_";
146 static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER_FULL_RANGE = R"_(
147 const mat3 uYuvToRgbColorTransform = mat3(
148  1.0000f, 1.0000f, 1.0000f,
149  0.0000f, -0.1646f, 1.8814f,
150  1.4746f, -0.5714f, 0.0000f
151 );
152 )_";
153 static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB = R"_(
154 
155 const int uApplyHdrToSdrToneMapping = 1;
156 
157 
158 // BT.2100 / BT.2020 HLG EOTF for one channel.
159 float hlgEotfSingleChannel(float hlgChannel) {
160  // Specification:
161  // https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
162  // Reference implementation:
163  // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=265-279;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
164  const float a = 0.17883277;
165  const float b = 0.28466892;
166  const float c = 0.55991073;
167  return hlgChannel <= 0.5 ? hlgChannel * hlgChannel / 3.0
168  : (b + exp((hlgChannel - c) / a)) / 12.0;
169 }
170 
171 // BT.2100 / BT.2020 HLG EOTF.
172 vec3 hlgEotf(vec3 hlgColor) {
173  return vec3(hlgEotfSingleChannel(hlgColor.r),
174  hlgEotfSingleChannel(hlgColor.g),
175  hlgEotfSingleChannel(hlgColor.b));
176 }
177 
178 // BT.2100 / BT.2020 PQ EOTF.
179 vec3 pqEotf(vec3 pqColor) {
180  // Specification:
181  // https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
182  // Reference implementation:
183  // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=250-263;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
184  const float m1 = (2610.0 / 16384.0);
185  const float m2 = (2523.0 / 4096.0) * 128.0;
186  const float c1 = (3424.0 / 4096.0);
187  const float c2 = (2413.0 / 4096.0) * 32.0;
188  const float c3 = (2392.0 / 4096.0) * 32.0;
189 
190  vec3 temp = pow(clamp(pqColor, 0.0, 1.0), 1.0 / vec3(m2));
191  temp = max(temp - c1, 0.0) / (c2 - c3 * temp);
192  return pow(temp, 1.0 / vec3(m1));
193 }
194 
195 // Applies the appropriate EOTF to convert nonlinear electrical values to linear
196 // optical values. Input and output are both normalized to [0, 1].
197 vec3 applyEotf(vec3 electricalColor) {
198  if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
199  return pqEotf(electricalColor);
200  } else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
201  return hlgEotf(electricalColor);
202  } else {
203  // Output red as an obviously visible error.
204  return vec3(1.0, 0.0, 0.0);
205  }
206 }
207 
208 // Apply the HLG BT2020 to BT709 OOTF.
209 vec3 applyHlgBt2020ToBt709Ootf(vec3 linearRgbBt2020) {
210  // Reference ("HLG Reference OOTF" section):
211  // https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-E.pdf
212  // Matrix values based on computeXYZMatrix(BT2020Primaries, BT2020WhitePoint)
213  // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/libs/hwui/utils/HostColorSpace.cpp;l=200-232;drc=86bd214059cd6150304888a285941bf74af5b687
214  const mat3 RGB_TO_XYZ_BT2020 =
215  mat3(0.63695805f, 0.26270021f, 0.00000000f, 0.14461690f, 0.67799807f,
216  0.02807269f, 0.16888098f, 0.05930172f, 1.06098506f);
217  // Matrix values based on computeXYZMatrix(BT709Primaries, BT709WhitePoint)
218  const mat3 XYZ_TO_RGB_BT709 =
219  mat3(3.24096994f, -0.96924364f, 0.05563008f, -1.53738318f, 1.87596750f,
220  -0.20397696f, -0.49861076f, 0.04155506f, 1.05697151f);
221  // hlgGamma is 1.2 + 0.42 * log10(nominalPeakLuminance/1000);
222  // nominalPeakLuminance was selected to use a 500 as a typical value, used
223  // in
224  // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/tonemap/tonemap.cpp;drc=7a577450e536aa1e99f229a0cb3d3531c82e8a8d;l=62,
225  // b/199162498#comment35, and
226  // https://www.microsoft.com/applied-sciences/uploads/projects/investigation-of-hdr-vs-tone-mapped-sdr/investigation-of-hdr-vs-tone-mapped-sdr.pdf.
227  const float hlgGamma = 1.0735674018211279;
228 
229  vec3 linearXyzBt2020 = RGB_TO_XYZ_BT2020 * linearRgbBt2020;
230  vec3 linearXyzBt709 =
231  linearXyzBt2020 * pow(linearXyzBt2020[1], hlgGamma - 1.0);
232  vec3 linearRgbBt709 = clamp((XYZ_TO_RGB_BT709 * linearXyzBt709), 0.0, 1.0);
233  return linearRgbBt709;
234 }
235 
236 // Apply the PQ BT2020 to BT709 OOTF.
237 vec3 applyPqBt2020ToBt709Ootf(vec3 linearRgbBt2020) {
238  float pqPeakLuminance = 10000.0;
239  float sdrPeakLuminance = 500.0;
240 
241  return linearRgbBt2020 * pqPeakLuminance / sdrPeakLuminance;
242 }
243 
244 vec3 applyBt2020ToBt709Ootf(vec3 linearRgbBt2020) {
245  if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
246  return applyPqBt2020ToBt709Ootf(linearRgbBt2020);
247  } else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
248  return applyHlgBt2020ToBt709Ootf(linearRgbBt2020);
249  } else {
250  // Output green as an obviously visible error.
251  return vec3(0.0, 1.0, 0.0);
252  }
253 }
254 
255 // BT.2100 / BT.2020 HLG OETF for one channel.
256 float hlgOetfSingleChannel(float linearChannel) {
257  // Specification:
258  // https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_HLG
259  // Reference implementation:
260  // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=529-543;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
261  const float a = 0.17883277;
262  const float b = 0.28466892;
263  const float c = 0.55991073;
264 
265  return linearChannel <= 1.0 / 12.0 ? sqrt(3.0 * linearChannel)
266  : a * log(12.0 * linearChannel - b) + c;
267 }
268 
269 // BT.2100 / BT.2020 HLG OETF.
270 vec3 hlgOetf(vec3 linearColor) {
271  return vec3(hlgOetfSingleChannel(linearColor.r),
272  hlgOetfSingleChannel(linearColor.g),
273  hlgOetfSingleChannel(linearColor.b));
274 }
275 
276 // BT.2100 / BT.2020, PQ / ST2084 OETF.
277 vec3 pqOetf(vec3 linearColor) {
278  // Specification:
279  // https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.inline.html#TRANSFER_PQ
280  // Reference implementation:
281  // https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/renderengine/gl/ProgramCache.cpp;l=514-527;drc=de09f10aa504fd8066370591a00c9ff1cafbb7fa
282  const float m1 = (2610.0 / 16384.0);
283  const float m2 = (2523.0 / 4096.0) * 128.0;
284  const float c1 = (3424.0 / 4096.0);
285  const float c2 = (2413.0 / 4096.0) * 32.0;
286  const float c3 = (2392.0 / 4096.0) * 32.0;
287 
288  vec3 temp = pow(linearColor, vec3(m1));
289  temp = (c1 + c2 * temp) / (1.0 + c3 * temp);
290  return pow(temp, vec3(m2));
291 }
292 
293 // BT.709 gamma 2.2 OETF for one channel.
294 float gamma22OetfSingleChannel(float linearChannel) {
295  // Reference:
296  // https://developer.android.com/reference/android/hardware/DataSpace#TRANSFER_GAMMA2_2
297  return pow(linearChannel, (1.0 / 2.2));
298 }
299 
300 // BT.709 gamma 2.2 OETF.
301 vec3 gamma22Oetf(vec3 linearColor) {
302  return vec3(gamma22OetfSingleChannel(linearColor.r),
303  gamma22OetfSingleChannel(linearColor.g),
304  gamma22OetfSingleChannel(linearColor.b));
305 }
306 
307 // Applies the appropriate OETF to convert linear optical signals to nonlinear
308 // electrical signals. Input and output are both normalized to [0, 1].
309 vec3 applyOetf(vec3 linearColor) {
310  if (uOutputColorTransfer == COLOR_TRANSFER_ST2084) {
311  return pqOetf(linearColor);
312  } else if (uOutputColorTransfer == COLOR_TRANSFER_HLG) {
313  return hlgOetf(linearColor);
314  } else if (uOutputColorTransfer == COLOR_TRANSFER_GAMMA_2_2) {
315  return gamma22Oetf(linearColor);
316  } else if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR) {
317  return linearColor;
318  } else {
319  // Output blue as an obviously visible error.
320  return vec3(0.0, 0.0, 1.0);
321  }
322 }
323 
324 vec3 yuvToRgb(vec3 yuv) {
325  const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
326  return clamp(uYuvToRgbColorTransform * (yuv - yuvOffset), 0.0, 1.0);
327 }
328 
329 vec4 convert_to_rgb(vec4 tex) {
330  vec3 srcYuv = tex.xyz;
331  vec3 opticalColorBt2020 = applyEotf(yuvToRgb(srcYuv));
332  vec4 opticalColor =
333  (uApplyHdrToSdrToneMapping == 1)
334  ? vec4(applyBt2020ToBt709Ootf(opticalColorBt2020), 1.0)
335  : vec4(opticalColorBt2020, 1.0);
336  vec4 transformedColors = opticalColor;
337  return vec4(applyOetf(transformedColors.rgb), 1.0);
338 })_";
339 
340 static inline QString bt2020shader(const Video::ImageFormat& d)
341 {
342  QString shader;
343  shader.reserve(8000);
344 
345  shader += SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER;
346  if(d.color_range == AVCOL_RANGE_MPEG)
347  shader += SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER_LIMITED_RANGE;
348  else
349  shader += SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER_FULL_RANGE;
350 
351  if(d.color_trc == AVCOL_TRC_SMPTE2084)
352  shader += "const int uInputColorTransfer = COLOR_TRANSFER_ST2084; \n";
353  else if(d.color_trc == AVCOL_TRC_GAMMA22)
354  shader += "const int uInputColorTransfer = COLOR_TRANSFER_GAMMA_2_2; \n";
355  else if(d.color_trc == AVCOL_TRC_LINEAR)
356  shader += "const int uInputColorTransfer = COLOR_TRANSFER_LINEAR; \n";
357  else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
358  shader += "const int uInputColorTransfer = COLOR_TRANSFER_HLG; \n";
359  else
360  shader += "const int uInputColorTransfer = COLOR_TRANSFER_GAMMA_2_2; \n";
361 
362  shader += "const int uOutputColorTransfer = COLOR_TRANSFER_GAMMA_2_2; \n";
363 
364  shader += SCORE_GFX_CONVERT_BT2020_TO_RGB;
365 
366  return shader;
367 }
368 static inline QString colorMatrix(const Video::ImageFormat& d)
369 {
370  // qDebug() << "colorMatrix:" << magic_enum::enum_name(d.color_space).data()
371  // << magic_enum::enum_name(d.color_primaries).data()
372  // << magic_enum::enum_name(d.color_range).data()
373  // << magic_enum::enum_name(d.color_trc).data()
374  // << magic_enum::enum_name(d.chroma_location).data();
375  // FIXME handle full color range for 601 / 709
376 
377  switch(d.color_space)
378  {
379  case AVCOL_SPC_RGB:
380  return "vec4 convert_to_rgb(vec4 tex) { return tex; }";
381  case AVCOL_SPC_BT709:
382  return SCORE_GFX_CONVERT_BT709_TO_RGB;
383  case AVCOL_SPC_FCC:
384  return SCORE_GFX_CONVERT_BT601_TO_RGB; // FIXME
385  case AVCOL_SPC_BT470BG:
386  return SCORE_GFX_CONVERT_BT601_TO_RGB;
387  case AVCOL_SPC_SMPTE170M:
388  return SCORE_GFX_CONVERT_BT601_TO_RGB;
389  case AVCOL_SPC_SMPTE240M:
390  // FIXME see wikipedia
391  return SCORE_GFX_CONVERT_BT709_TO_RGB;
392  case AVCOL_SPC_YCGCO:
393  return SCORE_GFX_CONVERT_BT709_TO_RGB; // FIXME
394  case AVCOL_SPC_BT2020_NCL:
395  return bt2020shader(d);
396  case AVCOL_SPC_BT2020_CL:
397  return bt2020shader(d); // FIXME
398  case AVCOL_SPC_SMPTE2085:
399  return bt2020shader(d); // FIXME
400  case AVCOL_SPC_CHROMA_DERIVED_NCL:
401  return bt2020shader(d); // FIXME
402  case AVCOL_SPC_CHROMA_DERIVED_CL:
403  return bt2020shader(d); // FIXME
404  case AVCOL_SPC_ICTCP:
405  return bt2020shader(d); // FIXME
406 
407  default:
408  case AVCOL_SPC_NB:
409  case AVCOL_SPC_UNSPECIFIED:
410  case AVCOL_SPC_RESERVED:
411  break;
412  }
413  return SCORE_GFX_BT709_MATRIX; // FIXME
414 }
415 }
Graphics rendering pipeline for ossia score.
Definition: Filter/PreviewWidget.hpp:12
Definition: VideoInterface.hpp:16