Ric
Ric

Reputation: 581

Is converting YCbCr to RGB reversible?

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

Upvotes: 3

Views: 3411

Answers (3)

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.

Upvotes: 1

silvaren
silvaren

Reputation: 730

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.

Upvotes: 1

beaker
beaker

Reputation: 16810

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)

Upvotes: 1

Related Questions