RGB and YUV formula conversion and derivation

Table of Contents

Introduction

Full Range formula derivation

Limit Range derivation

Verification Test

References

Introduction

There are many standards for conversion between RGB and YUV. The coefficients of different standards are different, and it is often easy to get confused. There are also differences between full range and limitrange. In fact, these conversion coefficients are derived and supported by theory, and are not coefficients directly given in the standard. This article mainly introduces the derivation of the formula.

Full Range formula derivation

From the definition of YUV, we can know that Y represents the proportion of red, green and blue, U represents the difference between blue and brightness Y, V represents the difference between red and brightness Y, then find the RGB conversion YUV is to find the mixing ratio. Converting YUV to RGB is its inverse change. As shown in the figure below, the formula shows how to use the mixing ratio to convert RGB to YUV, where KrKgKb is the mixing ratio of RGB.

f9de473af5574d8592c143fd74efc300.png

Among them, the three coefficients KrKgKb are the second row of the RGB to XYZ matrix. For RGB to XYZ conversion, please refer to [Selected] Color Space Conversion – From RGB to LCH – Brightness, Saturation and Chroma_rgb to lch-CSDN Blog. With these three coefficients, the coefficients for RGB to YUV conversion are determined. So RGB to YUV conversion is completely determined by the color coordinates of the color gamut. To calculate the coefficient of RGB2YUV, just invert YUV2RGB.

109bd823ffab4c44a5d9669033fd8aad.png

The reference code is as follows:

def GetFullRangeCoef(xy):
    matRGB2XYZ, matXYZ2RGB = GetRGBXYZMatrices(xy)

    rgb2yuv_coef = np.zeros((3,3), np.float32)
    rgb2yuv_coef[0, :] = matRGB2XYZ[1, :]
    kr = matRGB2XYZ[1, 0]
    kg = matRGB2XYZ[1, 1]
    kb = matRGB2XYZ[1, 2]
    rgb2yuv_coef[1, 0] = kr / (2*(kb - 1))
    rgb2yuv_coef[1, 1] = kg / (2*(kb - 1))
    rgb2yuv_coef[1, 2] = 0.5
    rgb2yuv_coef[2, 0] = 0.5
    rgb2yuv_coef[2, 1] = kg / (2*(kr - 1))
    rgb2yuv_coef[2, 2] = kb / (2 * (kr - 1))

    yuv2rgb_coef = np.linalg.inv(rgb2yuv_coef)

    return rgb2yuv_coef, yuv2rgb_coef

Limit Range derivation

The range of YCbCr is also called limit range and tv range, and the range of YUV is also called full range and pc range.

YUV in TVs is also called YCbCr (Cb is the abbreviation of ColorBlue, Cr is the abbreviation of ColorRed). YCbCr adjusts the range in order to solve the Gibbs phenomenon. The following figure illustrates that when the sine function simulates the original signal, the peak value of the waveform exceeds the original signal by 8.9 %, it is necessary to reduce the range of YCbCr to prevent overflow. For example, the range of 8-bit YUV is [0,255], then the range of 8-bit YCbCr is Y[16,235]UV[16,240], (255?235)/(235?16)=9.1 Slightly greater than 8.9%, 16/(235?16)=7.3 is slightly less than 8.9%.

62c49712e58c4eff9d166f6a11c4795a.png

The process of converting limit range YCbCr to RGB is as follows:

f47c09e4c47c4cfab471782a8b024812.png

Taking 8bit as an example, Yscale=255 / (235 – 16), Uscale=255 / (240 – 16), Vscale=255 / (240 – 16), Yoffset=-16, Uoffset=-128, Voffset=-128. RGB2YCbCr only needs to be inverted.

The reference code for deriving the limit range formula is as follows:

def GetLimitRangeCoef(yuv2rgb_full):
    scale_mat = np.zeros((3, 3), np.float)

    scale_mat[0, 0] = 255 / (235 - 16)
    scale_mat[1, 1] = 255 / (240 - 16)
    scale_mat[2, 2] = 255 / (240 - 16)

    yuv2rgb_limit = yuv2rgb_full@scale_mat
    rgb2yuv_limit = np.linalg.inv(yuv2rgb_limit)
    return rgb2yuv_limit, yuv2rgb_limit

Verification test

Our commonly used YUV and RGB conversion standards mainly include BT709, BT601, and BT2020. Their color coordinates are:

#BT709
    xysRGB = np.array([
        [0.64, 0.33],
        [0.30, 0.60],
        [0.15, 0.06],
        [0.3127, 0.3290]
    ])
    #BT2020
    xyBT2020 = np.array([
        [0.708, 0.292],
        [0.17, 0.797],
        [0.131, 0.046],
        [0.3127, 0.3290]
    ])
    #BT601
    xyNTSC = np.array([
        [0.67, 0.33],
        [0.21, 0.71],
        [0.14, 0.08],
        [0.3101, 0.3162]
    ])

Then the reference code is as follows:

def TestsRGB709():
    #BT709
    xysRGB = np.array([
        [0.64, 0.33],
        [0.30, 0.60],
        [0.15, 0.06],
        [0.3127, 0.3290]
    ])
    #BT2020
    xyBT2020 = np.array([
        [0.708, 0.292],
        [0.17, 0.797],
        [0.131, 0.046],
        [0.3127, 0.3290]
    ])
    #BT601
    xyNTSC = np.array([
        [0.67, 0.33],
        [0.21, 0.71],
        [0.14, 0.08],
        [0.3101, 0.3162]
    ])

    rgb2yuv_coef, yuv2rgb_coef = GetFullRangeCoef(xyNTSC)
    print('rgb2yuv_full:', np.round(rgb2yuv_coef, 4))
    print('yuv2rgb_full:', np.round(yuv2rgb_coef, 4))

    rgb2yuv_limit, yuv2rgb_limit = GetLimitRangeCoef(yuv2rgb_coef)
    print('rgb2yuv_limit:', np.round(rgb2yuv_limit, 4))
    print('yuv2rgb_limit:', np.round(yuv2rgb_limit, 4))

The results obtained by BT601 are as follows:

rgb2yuv_full:
[[ 0.2989 0.5866 0.1144]
 [-0.1688 -0.3312 0.5 ]
 [0.5 -0.4184 -0.0816]]
yuv2rgb_full:
[[ 1. -0. 1.4021]
 [1. -0.3455 -0.7145]
 [ 1. 1.7711 0. ]]
rgb2yuv_limit:
[[ 0.2567 0.5038 0.0983]
 [-0.1483 -0.291 0.4392]
 [0.4392 -0.3675 -0.0717]]
yuv2rgb_limit:
[[ 1.1644 -0. 1.5962]
 [1.1644 -0.3933 -0.8134]
 [ 1.1644 2.0162 0. ]]

Results obtained by BT709:

rgb2yuv_full:
[[ 0.2126 0.7152 0.0722]
 [-0.1146 -0.3854 0.5 ]
 [0.5 -0.4542 -0.0458]]
yuv2rgb_full:
[[ 1. -0. 1.5747]
 [1. -0.1873 -0.4682]
 [ 1. 1.8556 -0. ]]
rgb2yuv_limit:
[[ 0.1826 0.6142 0.062 ]
 [-0.1007 -0.3386 0.4392]
 [0.4392 -0.3989 -0.0403]]
yuv2rgb_limit:
[[ 1.1644 -0. 1.7927]
 [1.1644 -0.2132 -0.533]
 [ 1.1644 2.1124 -0. ]]

Results obtained by BT2020:

rgb2yuv_full:
[[ 0.2627 0.678 0.0593]
 [-0.1396 -0.3604 0.5 ]
 [0.5 -0.4598 -0.0402]]
yuv2rgb_full:
[[ 1. -0. 1.4746]
 [1. -0.1646 -0.5714]
 [ 1. 1.8814 0. ]]
rgb2yuv_limit:
[[ 0.2256 0.5823 0.0509]
 [-0.1227 -0.3166 0.4392]
 [0.4392 -0.4039 -0.0353]]
yuv2rgb_limit:
[[ 1.1644 -0. 1.6787]
 [1.1644 -0.1873 -0.6504]
 [ 1.1644 2.1418 0. ]]

It can be seen that the obtained coefficients are the same as those we commonly use, so as long as the color gamut is given, there is actually a set of corresponding conversion coefficients, which is completely determined by the size of the color gamut.

Reference materials:

HDR to SDR Practical Journey (4) YUV to RGB Matrix Derivation – Nuggets

Is the matrix circulating on the Internet wrong? A brief discussion on how to correctly derive the video YUV to RGB matrix – Zhihu