View Full Version : Converting YUV to RGB formulas?
zerowalker
26th January 2024, 07:34
This is off topic as it's not really for avisynth development,
but hopefully it's close enough for a pass, otherwise I can be taken down of course.
I'm trying to figure out the correct formula for converting YUV to RGB,
both Rec601/Rec709 with Limited/Full range.
The ones I have found doesn't seem to match what avisynth produces,
apart from one which is this:
R = 1.164 * (Y - 16) + 1.793 * (U - 128);
G = 1.164 * (Y - 16) - 0.534 * (U - 128) - 0.213 * (V- 128);
B = 1.164 * (Y - 16) + 2.115 * (V - 128);
(where R,G,B are rounded when converting to 8bit).
This produces as far as i can tell the same results Rec709 Limited.
But it doesn't really match the formula shown here: https://en.wikipedia.org/wiki/Y%E2%80%B2UV
And i also don't know how to make it full range, except for the Y part.
I tried looking at the avisynth code to figure this out,
but it was harder than I thought.
So was hoping to get some help here if possible:)
Sharc
26th January 2024, 08:26
Your matrix above applies for Rec709 limited range Y'CbCr -> Full range RGB conversion. (You may have mixed up U an V though, please double check)
U and V are from the analog realm; one should actually be using Cb and Cr in the digital realm. Sometimes interchangeably used, unfortunately.
For conversions you may want to take a look here:
https://web.archive.org/web/20120403123714/http:/www.equasys.de/colorconversion.html#YCbCr-RGBColorFormatConversion
or check out the tool here:
https://res18h39.netlify.app/color
Gavino
26th January 2024, 13:57
See also here: http://avisynth.nl/index.php/Color_conversions
DTL
26th January 2024, 15:41
Correct equations are in the free available ITU documents (601/709/2020) .
AVS+ narrow range in current builds is not completely correct (waiting for pinterf patch merging).
There possible very many forms of simplified equations for each use case and it is not easy to check the result. So if current ready to use simplified equations not provide correct/expected values - it is good to start from official documents from ITU.
Simple C-form (reference) in the decodeYUVtoRGB plugin - https://github.com/DTL2020/ConvertYUVtoRGB/blob/main/DecodeYV12toRGB.cpp
iY = *l_srcp_Y + Ybias; // Ybias = -16
iU = *l_srcp_U + UVbias; // UVbias=-128
iV = *l_srcp_V + UVbias; // UVbias=-128
int iR = iY + ((iV * Kr) >> 13);
int iB = iY + ((iU * Kb) >> 13);
int iG = iY - ((iU * Kgu) >> 13) - ((iV * Kgv) >> 13);
and Kr, Kgu, Kgb, Kb
/* Computing of (Kr, Kgu, Kgv, Kb) from (Yr and Yb) coefficients of YUV colour system:
Yr
Yg = 1.0 - (Yr + Yb)
Yb
Kb = (1.0 - Yb) * 2
Kr = (1.0 - Yr) * 2
Kgu = (Yb * Kb) / Yg
Kgv = (Yr * Kr) / Yg
for Digital UV (601/709/2020):
RatioUVtoY = 219.0/224.0
Kr,Kb,Kgu,Kgv *= RatioUVtoY
*/
if (Matrix == 0) // 601
{
fYr = 0.299f;
fYb = 0.114f;
}
else if (Matrix == 1) // 709
{
fYr = 0.2126f;
fYb = 0.0722f;
}
else if (Matrix == 2) // 2020 (ncl ?)
{
fYr = 0.2627f;
fYb = 0.0593f;
}
float fKb = (1.0f - fYb) * 2.0f;
float fKr = (1.0f - fYr) * 2.0f;
float fYg = 1.0f - (fYr + fYb);
Kr = (short)(fKr * fRatioUVtoY * UVgain * mulfac + 0.5f);
Kb = (short)(fKb * fRatioUVtoY * UVgain * mulfac + 0.5f);
Kgu = (short)(((fYb * fKb) / fYg) * fRatioUVtoY * UVgain * mulfac + 0.5f);
Kgv = (short)(((fYr * fKr) / fYg) * fRatioUVtoY * UVgain * mulfac + 0.5f);
UVgain=1.0, mulfac = double(1 << int_arith_shift); // integer aritmetic precision scale
pinterf
26th January 2024, 17:27
AVS+ narrow range in current builds is not completely correct (waiting for pinterf patch merging).
Actually, there was a patch on this area.
zerowalker
27th January 2024, 08:21
Your matrix above applies for Rec709 limited range Y'CbCr -> Full range RGB conversion. (You may have mixed up U an V though, please double check)
U and V are from the analog realm; one should actually be using Cb and Cr in the digital realm. Sometimes interchangeably used, unfortunately.
For conversions you may want to take a look here:
https://web.archive.org/web/20120403123714/http:/www.equasys.de/colorconversion.html#YCbCr-RGBColorFormatConversion
or check out the tool here:
https://res18h39.netlify.app/color
Always been confused with YUV and Y'CbCr,
Thought Y'CbCr was "Component Video" and YUV was, well the normal stuff, both Digital and Analogue where it's Y, U and V.
@DTV
Will try it out, was more steps there than "usual", will try to find the ITU documentations as well for the equations.
Sharc
27th January 2024, 12:31
Always been confused with YUV and Y'CbCr,
Thought Y'CbCr was "Component Video" and YUV was, well the normal stuff, both Digital and Analogue where it's Y, U and V.
RGB or YUV or Y'CbCr or Y'PbPr all belong to "component" video.
The symbols U and V are sometimes used quite loosely. Avisynth adopted U and V for the digital realm (probably for convenience and historic reasons) rather than Cb and Cr. For the discussion here you can assume U=Cb and V=Cr.
DTL
27th January 2024, 18:04
"Thought Y'CbCr was "Component Video""
Most important difference between Analog (abstract YUV or defined in 0..XXX mV voltage range typically with zero black and zero UV for math zero value) and Digital YUV (CbCr for Coded B-Y and R-Y?) in storage and transfer domains data form. The 601/709/2020 recs are about digital exchange formats for motion pictures - not only abstract YUV-descriptions. Analog YUV looks like exist in the equal scale Y and UV signals (as provided from RGB->YUV matrix). Digital YUV uses quantization and some Digital YUV systems uses non-equal scale for Y and UV data in the output (storage and transfer/distribution/interfacing) form.
For performance of processing in lowest possibe steps number real YUV<->RGB equations are significantly compacted.
Real steps are:
1. Decode YUV from Digital YUV storage domain into abstract YUV 0..1 float domain.
2. Apply YUV->RGB matrix conversion in abstract 0..1 float domain.
3. Convert (encode) RGB into output digital domain (or other required at output).
Some designers with good understanding in math can warp 1+2+3 in the single step processing by some compute engine. It make best processing performance but may suffer from issues if something important is missing. And also harder or impossible to debug in steps.
The 601/709/2020 recs define Digital (quantized form) YUV domain as having non-equal scale for Y and UV data (219 for Y and 224 for UV/CbCr). It may be designed to have a bit less quantization noise in UV low-bit data with natural colour gamuts for these systems (or for YUV as having larger colour gamut in compare with RGB).
So the very typical error for convertor designers is missing Y and UV input data normalization to equal scale. It is 219/224 ratio in the math displayed. The resulted error is not very big but visible even in 8bit samples.
zerowalker
29th January 2024, 07:32
"Thought Y'CbCr was "Component Video"...
Thanks for the explanation:)
When doing the y offset (Y-16) in float (0-1).
Isn't it (Y-(16/255))?
Cause when I do this Rec709 is valid,
but Rec601 is wrong by a tiny amount,
but (Y-(16/256)) makes it valid.
(valid as in, identical to avisynth).
EDIT:
Okay even PC.601 becomes a tiny bit wrong.
EDIT 2:
Okay it seems this happens even with ffmpeg, is it cause some small math error in avisynths conversion?
DTL
29th January 2024, 17:20
"When doing the y offset (Y-16) in float (0-1).
Isn't it (Y-(16/255))?"
0..1 range in float is 16 to 235 code values in 8bit Digital YUV (scaled + shifted/offsetted). 235-19=219.
Typically we subtract 16 and maps result of 0..219 to 0..1 float (so divide by 219.0). The range above 235 code values in storage Digital YUV 8bit is for super-whites (above nominal 100% white for SDR).
If you first convert 0..255 integer to float 0.0..255.0 it maybe also easy to subtract 16.0 and divide result to 219.0 (for performance - multiply to 1/219.0 constant). You will got black and nominal white in 0.0..1.0 range and negative under-blacks and 1.0+ super-whites.
For UV it is subtract 128 and divide by 224.
See https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf 2.5.3 and 2.5.4
"it seems this happens even with ffmpeg, is it cause some small math error in avisynths conversion?"
AVS and ffmpeg may use integer computing with less precision for performance. Try ConvertBits(32) before ConvertToRGB and check output float32 RGB values.
See also some posts around https://forum.doom9.org/showthread.php?p=1987720#post1987720 about YUV to RGB decoding in AVS. Some +-1 rounding/computing errors still visible in 8bits. As fix maybe used 10bits (12..16) computing and rounding to 8bits with ConvertBits(8). So it is possible to get exactly 16/180 8bit RGB narrow code values for all colours of ColorBarsHD YUV output.
Argaricolm
24th April 2024, 02:59
Formulas for rec 601:
R /= 255;
G /= 255;
B /= 255;
Y = R * .299 + G * .587 + B * .114;
U = (B - Y) / 1.772 + .5;
V = (R - Y) / 1.402 + .5;
Here you can do Y,U,V *= 255 to get 8 bit YUV, and round to int.
Clamping to 0,255 is not needed. I'v checked all results will be in 0...255 range.
and back to RGB:
U -= 0.5;
V -= 0.5;
R = Y + 1.402 * V;
G = Y - 0.114 * 1.772 / 0.587 * U - 0.299 * 1.402 / 0.587 * V;
B = Y + 1.772 * U;
Here you need to do R,G,B *= 255, round to int.
If you will not convert from float to int between conversions then they will be lossless.
But be sure to use rounding before converting to int (like Math.Round in C#). Don't use just float to int conversion without rounding.
WRONG:
int r = (int)R;
CORRECT:
int r = (int)Math.Round(R);
Then your conversions will be lossless (checked).
Rec 709:
Y = 0.2126 * R + 0.7152 * G + 0.0722 * B;
U = (B - Y) / 1.8556 + .5;
V = (R - Y) / 1.5748 + .5;
R = Y + 1.5748 * V;
G = Y - 0.0722 * 1.8556 / 0.7152 * U - 0.2126 * 1.5748 / 0.7152 * V;
B = Y + 1.8556 * U;
Rec 2020:
Y = 0.2627 * R + 0.6780 * G + 0.0593 * B;
U = (B - Y) / 1.8814 + .5;
V = (R - Y) / 1.4746 + .5;
R = Y + 1.4746 * V;
G = Y - 0.0593 * 1.8814 / 0.6780 * U - 0.2627 * 1.4746 / 0.6780 * V;
B = Y + 1.8814 * U;
All formulas were tested.
When you convert float value to INT you need to use clamping (ensure that value don't go over limit). For example 8 bit limits = 0-255, 10 bit limits 0 - 1023.
In normal cases value will not go over limit. But I'v found one source where it happens.
Like there is
Y = 233
U = 130
V = 142
This makes R to be > 255.
I don't know how it happend that source has such values. Maybe there was some other formula used. Or maybe it was just a bug in conversion software that created source. With clamping it looks normal.
Argaricolm
28th May 2024, 22:16
Also I see that there is a question how to convert result to full range.
As I'v found the documented correct way to do it is:
Convert YUV to RGB floats (without converting them to ints).
Then you will have limited RGB but within range 0.0 - 1.0.
And now you need to rerange it.
You can do it this way for each channel (R,G,B):
8 bit:
float lowlimit = 16.0 / 255.0;
float range = 219.0 / 255.0;
float R = (R - lowlimit) / range;
10 bit:
float lowlimit = 64.0 / 1023.0;
float range = 879.0 / 1023.0;
float R = (R - lowlimit) / range;
Then also documentation suggests to do this (midtones correction):
if (R <= 1.0 && R >= 0.018) 1.099 * pow(R, 0.45) - 0.099; else if (R < 0.018 R cf >= 0) R *= 4.5;
Then you can use formulas above to convert back to YUV (YUV will be full range).
Don't forget to set "Full range" in encoder settings so it will set such flag. Or most hardware will treat your YUV video as limited range (in staxrip you can set it in VUI settings).
When you convert float value to INT you need to use clamping (ensure that value don't go over limit). For example 8 bit limits = 0-255, 10 bit limits 0 - 1023.
In normal cases value will not go over limit. But I'v found one source where it happens.
Like there is
Y = 233
U = 130
V = 142
This makes R to be > 255.
I don't know how it happend that source has such values. Maybe there was some other formula used. Or maybe it was just a bug in conversion software that created source. With clamping it looks normal.
YUV colour space is larger in comparison of RGB. So if you make some legal range YUV from RGB and make some adjustments in the YUV colour space - you may get out of range RGB while trying to convert to RGB. The adjustments may be simple sharpness processing with over/under shoots in YUV.
The possible solution is to keep RGB in float so you will not get integer overflow. But you need to remember this RGB have out of range values and if you need integer RGB output - you need to apply some limiter.
If you read ITU docs this issue also described. And the limiter may be mot very simple RGB clamp to min..max but recommended to make more advanced like attempt to keep hue if possible and degrade saturation first.
So as some advanced YUV->RGB converter it is possible to design not simple clamp-mode but more smart HSB/HSL domain aware transform keeping more important colour data if possible and make less degradation to image.
The order of degragation (errors) is about
1. Saturation (less important ?)
2. Brightness
3. Hue (most important ?).
So the possible algorithm:
If (out of range YUV to RGB triplet detected) -> convert to HSB (?) and use some iterative process to get valid range RGB triplet with minimized delta-Hue and delta-Brightness errors (with some (more ?) weight to delta-Hue ?). It may be some time and compute complex but will give better result in compare with simple clamp to min..max.
Also this smart RGB limiter may be stand alone plugin so you can make YUV to RGB unlimited in RGBPS and process with smart-RGB limiter to go to integer RGB.
Convert YUV to RGB floats (without converting them to ints).
Then you will have limited RGB but within range 0.0 - 1.0.
narrow/limited is sort of only integer low-bits domain term. If you get RGB in 'abstract' 0.0 (black) and 1.0 (nominal white) it is in this 'abstract domain'. Not full and not limited/narrow (because typically floats are unlimited).
To convert 0..1.0 float to full integer RGB you simply multiply to 2^N where n is number of bits in the ingeter encoding. So black will be 0 and nominal white will be 255 in 8bit or 1023 in 10bit. And you will lost both under-blacks and over-whites.
wonkey_monkey
29th May 2024, 12:07
To convert 0..1.0 float to full integer RGB you simply multiply to 2^N where n is number of bits in the [integer] encoding. So black will be 0 and nominal white will be 255 in 8bit or 1023 in 10bit.
Not 2^N-1? And what rounding is most advisable?
Not 2^N-1? And what rounding is most advisable?
Yes - to get 255 from 1.0f it is multiplication to (2^8)-1 = 255. I am typically bad at +-1 errors in many cases. Sorry.
About rounder for float it is typically a good working addition of +0.5f (half of LSB value). So to round 254.8f to 255 integers it is typically passed 255.2+0.5f=255.7f to engine converting float to int. It looks like 'rounding to the nearest'.
Though it is only my current ideas - the industry documents have precomputed tables for 10 (and sometimes 8) bit YUV and RGB values for colour bars test pattern so any math may be checked with these tables.
Also some 'losses' check is for given 180/16 narrow RGB - conversion to (10bit ?) YUV and back to RGB must also return 180/16 RGB.
It is for 180/16 RGB 8bit colour bars:
ConvertBits(10)
ConvertToYUV() // in 10bits or more
ConvertToRGB() // in 10bits or more
ConvertBits(8)
The industry standards typically allow to get 180/16 RGB from 10bit YUV. But with 8bit RGB<->YUV conversion will not be lossless and give about +-1 LSB error.
Argaricolm
29th May 2024, 21:20
Then also documentation suggests to do this (midtones correction):
if (R <= 1.0 && R >= 0.018) 1.099 * pow(R, 0.45) - 0.099; else if (R < 0.018 R cf >= 0) R *= 4.5;
About this. Not quite sure if it should be done.
Its an OETF function. As I understand from docs its real light -> electronic signal transfer.
There is EOTF function that is reverse of OETF.
But strange thing is that OETF makes image light and EOTF makes it very dark.
Quite interesting results with OETF used after full range...
vBulletin® v3.8.11, Copyright ©2000-2025, vBulletin Solutions Inc.