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.
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.
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%.
The process of converting limit range YCbCr to RGB is as follows:
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