3

I was playing around with some different image formats and ran across something I found odd. When converting from RGB to YCbCr and then back to RGB, the results are very similar to what I started with (the difference in pixels values is almost always less than 4). However, when I convert from YCbCr to RGB and then back to YCbCr, I often get vastly different values. Sometimes a values will differ by over 40.

I'm not sure why this is. I was under the impression that the colors which could be expressed through YCbCr were a subset of those in RGB, but it looks like this is completely wrong. Is there some known subset of colors in YCbCr that can be converted to RGB and then back to their original values?

The code I'm using to convert (based on this site):

def yuv2rgb(yuv):
  ret = []
  for rows in yuv:
    row = []
    for y, u, v in rows:
      c = y - 16
      d = u - 128
      e = v - 128
      r = clamp(1.164*c +           1.596*e, 16, 235)
      g = clamp(1.164*c - 0.392*d - 0.813*e, 16, 240)
      b = clamp(1.164*c + 2.017*d          , 16, 240)
      row.append([r, g, b])
    ret.append(row)
  return ret

def rgb2yuv(rgb):
  ret = []
  for rows in rgb:
    row = []
    for r, g, b in rows:
      y = int( 0.257*r + 0.504*g + 0.098*b + 16)
      u = int(-0.148*r - 0.291*g + 0.439*b + 128)
      v = int( 0.439*r - 0.368*g - 0.071*b + 128)
      row.append([y, u, v])
    ret.append(row)
  return ret

EDIT:

I created a rudimentary 3D graph of this issue. All the points are spots with a value difference less than 10. It makes a pretty interesting shape. X is Cb, Y is Cr, and Z is Y.

Each point is a YCbCr value that converts nicely to RGB and back

Gabriel Devillers
  • 3,155
  • 2
  • 30
  • 53
Ric
  • 581
  • 5
  • 26
  • 1
    You've used `y` in your `yuv2rgb` calculations rather than `c`. I haven't tested it, so I don't know if that's the extent of the problem. – beaker Oct 04 '15 at 02:44
  • Related: https://stackoverflow.com/questions/10566668/lossless-rgb-to-ycbcr-transformation – Gabriel Devillers Aug 19 '22 at 13:57

3 Answers3

1

As far as I know you should be able to convert from and to both formats with minimal precision loss.

The site you have mentioned has another set of conversion formulas called "RGB to full-range YCbCr" and "Full-range YCbCr to RGB", I believe these are the ones you should use and I think it should enable you to convert forward and back without any problems.

EDIT:

Since those formulas haven't worked for you, I'll share the formulas that I use for conversion between RGB and YUV in android:

R = clamp(1 * Y +        0 * (U - 128) + 1.13983 * (V - 128), 0, 255);
G = clamp(1 * Y + -0.39465 * (U - 128) + -0.5806 * (V - 128), 0, 255);
B = clamp(1 * Y + 2.03211 * (U - 128) +       0 * (V - 128), 0, 255);

Y = clamp(0.299    * R + 0.587    * G + 0.114    * B, 0, 255);
U = clamp(-0.14713 * R + -0.28886 * G + 0.436    * B + 128, 0, 255);
V = clamp(0.615    * R + -0.51499 * G + -0.10001 * B + 128, 0, 255);

I've just tried and it seems to work back and forth. Notice the sums and subtractions of 128, because this YUV representation is composed of unsigned byte ranges (0..255) and so is RGB (as usual), so if you really need a (16..235) and (16..240) ranges for your YCbCr you might need another formula.

silvaren
  • 730
  • 6
  • 14
  • Thanks for the advice! Sadly, using the other formulas don't work either. For example, YCbCR values [0,0,0] become RGB values [0,134,0], which then convert back to YCbCR values of [78, 83, 71]. – Ric Oct 07 '15 at 20:58
  • 1
    @Ric I have edited my response and included the formulas I have personally used before in android, you might want to check them. – silvaren Oct 08 '15 at 06:18
  • thanks for the code. I tried it out on YUV values of [16, 16, 16]. Converting to RGB gives [0, 125, 243], which then converts back to [101, 198, 39]. – Ric Oct 08 '15 at 15:45
  • 1
    You are right. First I had a typo in the 2.03211 element, which is supposed to be positive. Not only that, but you are also right that the values are not converted back to the same values, *but* from my experience it ends up mapping to the same color. Try converting YUV1 -> RGB1 -> YUV2 -> RGB2, then RGB1 and RGB2 should be approximately the same value. From your example (16,16,16)->(0,125,0)->(73,92,64)->(0,124,0). – silvaren Oct 09 '15 at 00:20
1

As I said in my comment, your first problem is that you're using y rather than c for calculations inside the loop in yuv2rgb.

The second problem is that you're clamping the RGB values to the wrong range: RGB should be 0..255.

The RGB calculations should look like this:

  r = clamp(1.164*c +           1.596*e, 0, 255)
  g = clamp(1.164*c - 0.392*d - 0.813*e, 0, 255)
  b = clamp(1.164*c + 2.017*d          , 0, 255)
beaker
  • 16,331
  • 3
  • 32
  • 49
  • Great catch on the typo! Sadly clamping to that range still yields non-reversible results. YCbCr values at [0,0,0] go to RGB at [0,135,0], which give final YCbCr values of [84, 88, 78] – Ric Oct 07 '15 at 21:01
  • @Ric `[0, 0, 0]` isn't a valid YCbCr color value. The minimum value for each is 16. – beaker Oct 07 '15 at 21:14
1

No, it is not, at all. [All disscused below is for 8 bit.] And it is obvious in the case of Full range R'G'B' to limited range YCbCr (there is just no bijection). For example, you can test it here:

https://res18h39.netlify.app/color

Full range R'G'B' values 238, 77, 45 are encoded into limited YCbCr with BT.601 matrix: you will get limited range 120, 90, 201 after school rounding, but if you will round it you will get 238, 77, 44 back in R'G'B'. And 238, 77, 44 value will go to the same value. Oopsie. Here it is: game over.

In the case with full range RGB to full range YCbCr... There are some values in YCbCr that will be negative R', G', B'. (For example in limited range YCbCr BT.709 values 139, 151, 24 will be limited (!) RGB -21, 182, 181 and full RGB -43, 194, 192.) So again, no bijection.

Next, limited range R'G'B' to limited range YCbCr... Again, no bijection. Black in YCbCr is actually 16, 128, 128 and only this. All other 16, x, y are not allowed [they are in xvYCC, which is non-standard], while they are in R, G, B, and all 235, 128, 128 the same. And the previous negative R', G', B' also applies, of course.

And with limited range to full range, I do not know.