YCbCr to RGB color matrix computation¶
Mathematic formulas¶
Video range constants:
Yoff = 16
Yrange = 219
UVrange = 224
Full range constants:
Yoff = 0
Yrange = 255
UVrange = 255
8-bit quantized Y,Cb,Cr:
Yq = Round((Y *255 - Yoff) / Yrange)
Cbq = Round((Cb*255 - 128) / UVrange)
Crq = Round((Cr*255 - 128) / UVrange)
BT.709 formula (deciphered from the constants in the paper):
Yq = Kr*R + Kg*G + Kb*B
Cbq = (B-Yq) / (2*(1-Kb))
Crq = (R-Yq) / (2*(1-Kr))
Note: we also have Kg == 1 - Kb - Kr
Inverting the equations to get R
, G
, B
:
R = Yq + Crq * 2*(1-Kr)
G = Yq/Kg - Kr*R/Kg - Kb*B/Kg
B = Yq + Cbq * 2*(1-Kb)
Expressing them according to Y
, Cr
, Cb
(WARNING: quantization rounding is ignored):
R = Yq + Crq * 2*(1-Kr)
= (Y * 255 - Yoff) / Yrange + (Cr*255 - 128) * 2*(1-Kr) / UVrange
= Y * 255/Yrange - Yoff/Yrange + Cr * 255*2*(1-Kr)/UVrange - 128*2*(1-Kr)/UVrange
= Y * 255/Yrange + Cr * 255*2*(1-Kr)/UVrange + (-Yoff/Yrange - 128*2*(1-Kr)/UVrange)
`---.----' `--------.---------' `-----------------.-----------------'
Y factor Cr factor Offset
G = Yq/Kg - Kr*R/Kg - Kb*B/Kg
= Yq/Kg - Kr*(Yq + Crq * 2*(1-Kr))/Kg - Kb*(Yq + Cbq * 2*(1-Kb))/Kg
= Yq/Kg - Kr*Yq/Kg - Crq * Kr*2*(1-Kr)/Kg - Kb*Yq/Kg - Cbq * Kb*2*(1-Kb)/Kg
= Yq/Kg - Kr*Yq/Kg - Kb*Yq/Kg - Cbq * Kb*2*(1-Kb)/Kg - Crq * Kr*2*(1-Kr)/Kg
= Yq*(1 - Kr - Kb)/Kg + Cbq * -Kb*2*(1-Kb)/Kg + Crq * -Kr*2*(1-Kr)/Kg
= (Y * 255 - Yoff) * (1 - Kr - Kb)/Kg / Yrange + Cbq * -Kb*2*(1-Kb)/Kg + Crq * -Kr*2*(1-Kr)/Kg
= Y * 255*(1-Kr-Kb)/(Yrange*Kg) + Cbq * -Kb*2*(1-Kb)/Kg + Crq * -Kr*2*(1-Kr)/Kg - Yoff*(1-Kr-Kb)/(Yrange*Kg)
= Y * 255*(1-Kr-Kb)/(Yrange*Kg) + (Cb*255 - 128) / UVrange * -Kb*2*(1-Kb)/Kg + (Cr*255 - 128) / UVrange * -Kr*2*(1-Kr)/Kg - Yoff*(1-Kr-Kb)/(Yrange*Kg)
= Y * 255*(1-Kr-Kb)/(Yrange*Kg) + Cb * 255*-Kb*2*(1-Kb)/Kg/UVrange + Cr * 255*-Kr*2*(1-Kr)/Kg/UVrange - (Yoff*(1-Kr-Kb)/(Yrange*Kg) + 128*-Kb*2*(1-Kb)/Kg/UVrange + 128*-Kr*2*(1-Kr)/Kg/UVrange)
= Y * 255*Kg/(Yrange*Kg) + Cb * -255*Kb*2*(1-Kb)/(Kg*UVrange) + Cr * -255*Kr*2*(1-Kr)/(Kg*UVrange) + (-Yoff*Kg/(Yrange*Kg) + (128*Kb*2*(1-Kb) + 128*Kr*2*(1-Kr))/(UVrange*Kg)
= Y * 255*/Yrange + Cb * -255*Kb*2*(1-Kb)/(Kg*UVrange) + Cr * -255*Kr*2*(1-Kr)/(Kg*UVrange) + (-Yoff/Yrange + (128*2*Kb*((1-Kb) + (1-Kr)))/(UVrange*Kg)
`----.----' `-------------.-------------' `------------.--------------' `--------------------------.----------------------------'
Y factor Cb factor Cr factor Offset
B = Yq + Cbq * 2*(1-Kb)
= <similarly to R>
= Y * 255/Yrange + Cb * 255*2*(1-Kb)/UVrange + (-Yoff/Yrange - 128*2*(1-Kr)/UVrange)
`---.----' `--------.---------' `-----------------.-----------------'
Y factor Cb factor Offset
Conversion matrix¶
Using previous expressions, we can craft the following 4x4
matrix:
Red |
Green |
Blue |
Alpha |
---|---|---|---|
Y factor |
Y factor |
Y factor |
0 |
Cb factor |
Cb factor |
Cb factor |
0 |
Cr factor |
Cr factor |
Cr factor |
0 |
Offset |
Offset |
Offset |
1 |
Which can be used with colormatrix * vec4(yuv, 1.0)
to obtain the RGB value.
Exact values computation¶
Using intermediate variables to factor out common parts of the formulas, and
with the help of the Python fractions
module, we can make the following code
to compute accurate matrices:
from fractions import Fraction
K_constants = dict(
bt601=(
Fraction(299, 1000),
Fraction(587, 1000),
Fraction(114, 1000),
),
bt709=(
Fraction(2126, 10000),
Fraction(7152, 10000),
Fraction( 722, 10000),
),
bt2020=(
Fraction(2627, 10000),
Fraction(6780, 10000),
Fraction( 593, 10000),
),
)
def _get_matrix(name, video_range=True):
Kr, Kg, Kb = K_constants[name]
assert Kg == 1 - Kb - Kr
y_off, y_range, uv_range = (16, 219, 224) if video_range else (0, 255, 255)
y_factor = Fraction(255, y_range)
r_scale = Fraction(2 * (1 - Kr), uv_range)
b_scale = Fraction(2 * (1 - Kb), uv_range)
g_scale = Fraction(2, Kg * uv_range)
y_off = Fraction(-y_off, y_range)
r_y_factor = g_y_factor = b_y_factor = y_factor
r_cb_factor = 0
g_cb_factor = -255 * g_scale * Kb * (1 - Kb)
b_cb_factor = 255 * b_scale
r_cr_factor = 255 * r_scale
g_cr_factor = -255 * g_scale * Kr * (1 - Kr)
b_cr_factor = 0
r_off = y_off - 128 * r_scale
g_off = y_off + 128 * g_scale * (Kb * (1 - Kb) + Kr * (1 - Kr))
b_off = y_off - 128 * b_scale
return [
r_y_factor, g_y_factor, b_y_factor, Fraction(0),
r_cb_factor, g_cb_factor, b_cb_factor, Fraction(0),
r_cr_factor, g_cr_factor, b_cr_factor, Fraction(0),
r_off, g_off, b_off, Fraction(1),
]
This code is re-written in C in libnopegl
, with a float
precision approximation.
Accuracy testing¶
The matrices found in test_colorconv
are generated using the following code:
_matrix_tpl = ''' [{}] = {{
{:>16}., {:>20}., {:>16}., {}.,
{:>16}., {:>20}., {:>16}., {}.,
{:>16}., {:>20}., {:>16}., {}.,
{:>16}., {:>20}., {:>16}., {}.,
}},'''
_range_tpl = ''' [{}] = {{
{}
}},'''
def _gen_tables():
cspaces = ("bt601", "bt709", "bt2020")
ranges = []
for r in (True, False):
matrices = []
for i, cspace in enumerate(cspaces):
matrices.append(_get_matrix(cspace, video_range=r))
matrix_defs = '\n'.join(_matrix_tpl.format(i, *mat) for i, mat in enumerate(matrices))
ranges.append(matrix_defs)
defs = '\n'.join(_range_tpl.format(i, matrices) for i, matrices in enumerate(ranges))
print(defs)
if __name__ == "__main__":
_gen_tables()
These matrices are used as a reference to evaluate the precision of the C code. Using fractions instead of float in C has been considered (and tested) but requires an uncessary complexity (overflow handling in particular) for marginal accuracy benefits.