Loading...
Searching...
No Matches
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
7namespace score::gfx
8{
9/* OLD code for reference
10// 601
11const vec3 R_cf = vec3(1.164383, 0.000000, 1.596027);
12const vec3 G_cf = vec3(1.164383, -0.391762, -0.812968);
13const vec3 B_cf = vec3(1.164383, 2.017232, 0.000000);
14const vec3 offset = vec3(-0.0625, -0.5, -0.5);
15yuv += offset;
16r = dot(yuv, R_cf)
17g = dot(yuv, G_cf)
18b = dot(yuv, B_cf)
19
20// OR (cf wiki https://en.wikipedia.org/wiki/YCbCr)
21const vec3 offset = vec3(0., -0.5, -0.5);
22const 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
28const 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);
33const 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
48OR
49
50
51 fragColor = vec4(coeff * (vec3(y,u,v) + offset), 1);
52
53OR
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
68OR
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
132static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER = R"_(
133const int COLOR_TRANSFER_LINEAR = 1;
134const int COLOR_TRANSFER_GAMMA_2_2 = 10;
135const int COLOR_TRANSFER_ST2084 = 6;
136const int COLOR_TRANSFER_HLG = 7;
137)_";
138
139static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER_LIMITED_RANGE = R"_(
140const 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)_";
146static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB_HEADER_FULL_RANGE = R"_(
147const 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)_";
153static constexpr auto SCORE_GFX_CONVERT_BT2020_TO_RGB = R"_(
154
155const int uApplyHdrToSdrToneMapping = 1;
156
157
158// BT.2100 / BT.2020 HLG EOTF for one channel.
159float 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.
172vec3 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.
179vec3 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].
197vec3 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.
209vec3 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.
237vec3 applyPqBt2020ToBt709Ootf(vec3 linearRgbBt2020) {
238 float pqPeakLuminance = 10000.0;
239 float sdrPeakLuminance = 500.0;
240
241 return linearRgbBt2020 * pqPeakLuminance / sdrPeakLuminance;
242}
243
244vec3 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.
256float 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.
270vec3 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.
277vec3 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.
294float 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.
301vec3 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].
309vec3 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
324vec3 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
329vec4 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
340static 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}
368static 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_CONVERT_BT709_TO_RGB; // FIXME
414}
415}
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:12
Definition VideoInterface.hpp:16