skip to main content

Color correction fails in unbounded sRGB

Unless an unwanted color cast was created in the sRGB color space, sRGB is the wrong color space to use when removing the color cast. The fact that unbounded sRGB encodes out of gamut colors using negative channel values makes a bad situation worse: Color casts are corrected using multiplication, and multiplying out of gamut colors in the unbounded sRGB color space produces meaningless results.

Written May 2014.

This article is part of a series of articles on the limitations of unbounded sRGB as a universal color space for image editing. However, the problems illustrated below are more broadly applicable: Color balancing an image is a chromaticity-dependent editing operation and so an unwanted color cast should always be corrected in the RGB working space in which it was created.

Introduction: When correcting an image color cast, the RGB working space you use does matter

A color cast is created by multiplying an image layer by a particular color. That same color cast is removed by multiplying the image layer by the inverse of that same color.

The inverse of a color is the same color (has the same location in the XYZ reference color space) before and after an image is converted from one RGB color to another. But the RGB channel values are different. This has the perhaps unexpected consequence that a color cast that is created in any given RGB working space can't be correctly removed in some other RGB working space. The problem, of course, is that multiplication is a chromaticity-dependent editing operation: in other words, the result of multiplying two colors depends on the chromaticities of the color space in which the multiply operation is performed.

Occasionally a software developer will conclude that because unbounded sRGB (also called "extended sRGB") can be used to encode and display all possible colors, therefore it's also suitable as a "universal working space" for editing all images. However, except in the limiting case where the color cast was created in the sRGB color space (bounded or unbounded), color correction fails in the unbounded sRGB color space. If the image has any colors that are out of gamut with respect to the bounded sRGB color space, and especially if the color cast was created using a color that's out of gamut with respect to the bounded sRGB color space, then correcting a color cast in the unbounded sRGB color space produces not just wrong results, but spectacularly wrong results.

Software used in this article:

High bit depth GIMP 2.9 from git is unusual among image editors in that it actually does allow editing images that have been converted to the unbounded sRGB color space.

The default GIMP from git uses linear gamma RGB values for some editing operations and uses "gamma corrected" RGB values (using the default sRGB almost gamma=2.2 tone reproduction curve) for other editing operations. For images presented on this page I compiled and used a version of GIMP from git that I modified to ensure that all editing operations were done using linear gamma processing.

Removing five different cyan color casts created in five different RGB working spaces

Figure 1 below shows a picture of two men sitting at an Oxygen Bar. I'm not sure what people do at Oxygen Bars, but that's not the point. The point is both men are wearing shorts. So when the image is given various color casts in different RGB working spaces, it will be easy to see the incorrect skin tones that result from trying to remove these color casts in the unbounded sRGB color space. The white dot in the lower right corner of the picture provides a known "should be white" patch for using with the Levels "Pick white point" eyedropper.

Two men sitting at an Oxygen Bar. The white dot should make it trivially easy to correct any color cast that might be given to the image. The image will be converted in turn to five different RGB working spaces. In each working space, the image will be given a cyan color cast by using Levels and moving the Red Channel Output slider from 100.00 to 50.00. Consequently the exact color of the cyan color cast will vary from one working space to the next. Then the five images will be converted to unbounded sRGB for color correction.

Figure 2 below has two columns. The left column shows the result of giving the original image a cyan color cast in five different RGB working spaces. The right column shows the result of using the Levels "Pick White Point" eyedropper to correct the color casts after the five images were converted to unbounded sRGB:

Correcting a color cast needs to be done in the color space in which the color cast was created. Only color casts that were created in the sRGB color space can be corrected in the unbounded sRGB color space. The left Column shows crops from the test image, each time with a cyan color cast that was created in a different RGB working space. The right Column shows the results of "correcting" the various cyan color casts in the unbounded sRGB color space using the Levels "Pick white point" eyedropper. The cyan dot does turn white, but except for the image in the first row, the remainding parts of the "corrected" image show damaged colors:

From top to bottom, a summary of the results of attempting to color correct the various cyan casts in the unbounded sRGB color space:

  • Cyan color cast 1 was created in the linear gamma sRGB color space:

    Color casts created in the sRGB color space can be easily color-corrected in the unbounded sRGB color space.

  • Cyan color cast 2 was created in the linear gamma AdobeRGB1998 color space and is in-gamut with respect to the bounded sRGB color space (the unbounded sRGB channel values are between 0.0 and 1.0, inclusively):

    Attempting to correct the AdobeRGB1998 cyan color cast in the unbounded sRGB color space made the skin tones just a tiny bit too orange. The orange color cast does not look too obviously wrong on the men's legs. But imagine if you were trying to color correct a cyan color cast in a fair-skinned baby's face — the orange cast would be unacceptable as well as wrong.

  • Cyan color cast 3 was created in the linear gamma Rec. 2020 color space. This cyan color cast is out of gamut with respect to display-referred image editing (some of the channel values are greater than 1.0), but in gamut with respect to scene-referred image editing (none of the channel values are less than 0.0):

    Attempting to correct the Rec. 2020 cyan color cast in the unbounded sRGB color space made the skin tones noticeably pink-orange, and the tan chair is mottled with cyan and pink.

  • Cyan color cast 4 was created in the linear gamma WideGamutRGB color space. This cyan color cast is out of gamut with respect to display-referred image editing (some of the channel values are greater than 1.0), but in gamut with respect to scene-referred image editing (none of the channel values are less than 0.0):

    Attempting to correct the WideGamutRGB cyan color cast in the unbounded sRGB color space made the skin tones vivid pink-orange, and the tan chair very mottled with cyan and pink. Also the man's gray-beige shorts are now mottled with cyan and pink patches and the gray metal front of the bar looks dark cyan-green instead of dark gray.

  • Cyan color cast 5 was created in the linear gamma ProPhotoRGB color space. This cyan color cast is out of gamut with respect to display-referred and scene-referred image editing (one of the channel values is less than 0.0):

    Attempting to correct the ProPhotoRGB cyan color cast in the unbounded sRGB color space made the skin tones and the tan chair turn vivid turquoise green. The gray-beige shorts turned to a patchwork of pink and cyan-green. The gray metal front of the bar and the man's faded navy t-shirt both turned red.

If the images had all been corrected in the color spaces in which the cyan casts were created, each cyan color cast would have been properly removed and the resulting image would have looked exactly like the original image in Figure 1 above.

Why the chromaticities matter when color correcting images

The mathematics of correcting a color cast

Regardless of what UI function you might use to give an image a color cast, somewhere "under the hood" the color cast was created by multiplying the image by the appropriate color.

Likewise, regardless of what UI function you might use to remove a color cast, what really happens is that "somewhere under the hood" the image is multiplied by a color equal to the inverse of the color cast. For example, here's what really happens when you use the Levels White Point Picker to click on a nominally neutral spot in an image:

  1. The spot's uncorrected RGB channel values are determined. Let's say the resulting channel values are: (R1, G1, B1).
  2. A color is calculated that, when multiplied by the uncorrected RGB channel values, produces unity: (1/R1, 1/G1, 1/B1) = (R2, G2, B2).
  3. The image is multiplied by the calculated color (R2, G2, B2).

When the above procedure is performed in the color space in which the color cast was created, the color cast is removed. But when the procedure is performed in a different color space, the color cast is not removed. Instead any non-neutral color is more or less damaged. This is because the results of multiply depend on the color's space's chromaticities.

When attempting to remove a color cast in the wrong color space, neutral colors (black, white, and gray) are indeed made neutral. But all the other colors are systematically made incorrect. The greater the differences between the color space in which the color cast was created and the color space you use when trying to remove the color cast, the worse the results will be.

When trying to remove a color cast that was created in some other color space after the image is converted to the unbounded sRGB color space, if the color cast itself is out of gamut with respect to the bounded sRGB color gamut, then the resulting image colors are not just damaged but also meaningless. This is because multiplying out of gamut colors produces meaningless results.

Using unbounded sRGB to correct a cyan color cast that was created in the sRGB color space

First let's consider the version of the Oxygen Bar image above that was given a cyan color cast in the sRGB color space (the first row of Figure 2 above):

  1. Originally the image was in the sRGB color space.
  2. Before the image was given the cyan color cast in the sRGB color space, the white dot eyedroppers as (1.000000, 1.000000, 1.000000).
  3. Also before the image was given the cyan color cast in the sRGB color space, a selected spot on the man's leg eyedroppers as (0.347365, 0.199789, 0.160058). In the XYZ reference color space this sRGB color has the location (0.251302, 0.230215, 0.138496). Let's call this XYZ location our "target color".
  4. After the image was given the cyan color cast in the sRGB color space, the now-cyan dot eyedroppers as (0.500000, 1.000000, 1.000000) and the man's leg eyedroppers as (0.173682, 0.199789, 0.160058) and .
  5. Using the Levels "White point picker" on the now-cyan dot to correct the cyan color cast is the same as multiplying the image by (1/0.500000, 1/1.000000, 1/1.000000), which equals (2.000000, 1.000000, 1.000000). Doing so of course makes the cyan dot white again.
  6. Multiplying the channel values of the selected spot on the man's leg by (2.000000, 1.000000, 1.000000) gives (0.347364, 0.199789, 0.160058).
  7. After using the Levels White point picker on the cyan dot, as expected the man's leg eyedroppers (0.347364, 0.199789, 0.160058), which of course still has the XYZ location (0.251302, 0.230215, 0.138496). So we successfully removed the cyan color cast and got back to the target color.

In the unbounded sRGB color space, removing a cyan color cast that was created in the sRGB color space is trivially easy.

Using unbounded sRGB to correct a cyan color cast that was created in the Rec. 2020 color space

Now let's consider the version of the Oxygen Bar image above that was given a cyan color cast in the Rec. 2020 color space (the third row of Figure 2 above):

  1. Originally the image was in the Rec. 2020 color space. Before the image was given a cyan color cast in the Rec. 2020 color space, the white dot eyedroppers as (1.000000, 1.000000, 1.000000).
  2. Before the image was given a cyan color cast in the Rec. 2020 color space, a selected spot on the man's leg eyedroppers as (0.290654 0.209538 0.166624). In the XYZ reference color space this Rec. 2020 color has the channel values (0.251302 0.230215 0.138495). Let's call this XYZ location our "target color".
  3. After the image was given the cyan color cast in the Rec. 2020 color space, the now-cyan dot eyedroppers as (0.500000, 1.000000, 1.000000) and the selected spot on the man's leg eyedroppers as (0.145327 0.209538 0.166624).
  4. After the image is converted to unbounded sRGB, the cyan dot eyedroppers as (0.169710, 1.062298, 1.009072) and the previously selected spot on the man's leg eyedroppers as (0.106037 0.217897 0.162695).
  5. Using the Levels "White point picker" on the cyan dot to correct the cyan color cast is the same as multiplying the image by (1/0.169710, 1/1.062298, 1/1.009072) = (5.892405, 0.941355, 0.991010). Doing so of course makes the cyan dot white again.
  6. Multiplying the channel values of the selected spot on the man's leg by (5.892405, 0.941355, 0.991010) gives (0.624813, 0.205118, 0.161232). Eyedroppering the same spot after using the Levels White point picker on the cyan dot gives (0.624817, 0.205118, 0.161232), which departs from our predicted color by (0.000004, 0.000000, 0.000000). We will assume a rounding error accounts for this very small difference.
  7. Converting the sRGB color (0.624817, 0.205118, 0.161232) to the XYZ color space gives (0.374501, 0.295836, 0.143712), which isn't particularly close to our target color of (0.251302 0.230215 0.138495).

In the unbounded sRGB color space, we didn't succeed in removing the cyan color cast that was created in the Rec. 2020 color space. Well, we did manage to make the cyan dot white. But the rest of the image colors are all messed up. Of course we already knew this just by looking at the third row of Figure 2 above. But now we know the mathematics that prevent correcting a Rec. 2020-created cyan color cast after the image is converted to unbounded sRGB.

Using unbounded sRGB to correct a cyan color cast that was created in the ProPhotoRGB color space

Now let's consider the version of the Oxygen Bar image above that was given a cyan color cast in the ProPhotoRGB color space (the fifth row of Figure 2 above):

  1. Originally the image was in the ProPhotoRGB color space. Before the image was given a cyan color cast in the ProPhotoRGB color space, the white dot eyedroppers as (1.000000, 1.000000, 1.000000).
  2. Before the image was given a cyan color cast in the ProPhotoRGB color space, a selected spot on the man's leg eyedroppers as (0.272312 0.213187 0.167892). In the XYZ reference color space this ProPhotoRGB color has the channel values (0.251301 0.230215 0.138496). Let's call this XYZ location our "target color". For those of you with very sharp eyes, I'm using the ArgyllCMS xicclu utility to ascertain the XYZ values; xicclu gives results to 6 decimal places, so let's assume the difference in the red channel of 0.000001 between the XYZ location of the selected spot on the man's leg in the ProPhotoRGB color space and the Rec. 2020 and sRGB color spaces is a rounding error.
  3. After the image was given the cyan color cast in the ProPhotoRGB color space, the now-cyan dot eyedroppers as (0.500000, 1.000000, 1.000000) and the selected spot on the man's leg eyedroppers as (0.136175 0.213191 0.167893).
  4. After the image is converted to unbounded sRGB, the cyan dot eyedroppers as (-0.017195, 1.114419, 1.004272) and the previously selected spot on the man's leg eyedroppers as (0.070405 0.230948 0.161221).
  5. Using the Levels "White point picker" on the cyan dot to correct the cyan color cast is the same as multiplying the image by (1/-0.017195, 1/1.114419, 1/1.004272) = (-58.156441, 0.897329, 0.995746). Doing so of course makes the cyan dot white again.
  6. Multiplying the channel values of the selected spot on the man's leg by (-58.156441, 0.897329, 0.995746) gives (-4.094504, 0.207236, 0.160535). Eyedroppering the same spot after using the Levels White point picker on the cyan dot gives (-4.094407 0.207236 0.160536), which is an exact match in the Green and Blue channels and off by 0.000097 in the Red channel. Considering that we are dealing with completely meaningless numbers here, I'll assume there's a rounding error somewhere in the chain between the eyedropper readout and GIMP's ability to accurately multiply imaginary floating point RGB values.
  7. The ArgyllCMS xicclu utility doesn't calculate XYZ values for meaningless RGB values such as (-4.094407 0.207236 0.160536). LCMS's transicc (which requires multiplying the channel values by 255 for input and dividing by 100 for output equivalent to xicclu's output), gives (-1.682531, -0.752660, 0.077748). This XYZ location represents an imaginary color with a physically impossible Lumunance of Y = -0.752660.

In the unbounded sRGB color space, we didn't succeed in removing the cyan color cast that was created in the ProPhotoRGB color space. Well, we did manage to make the cyan dot white. But the rest of the image colors are completely meaningless because in the unbounded sRGB color space we needed to multiply by an out of gamut color to make the cyan dot white. Of course we already knew the colors were meaningless just by looking at the fifth row of Figure 2 above. But now we know the mathematics that prevent correcting a ProPhotoRGB-created cyan color cast after the image is converted to unbounded sRGB.

Conclusion: Color correction fails in the unbounded sRGB color space

Except for the limiting case where the color cast was actually created in the sRGB color space, color correction fails in the unbounded sRGB color space for two very different reasons:

  1. Color correction depends on multiplication, and the result of multiplying two colors depends on the color space chromaticities:
    • Color casts are created by multiplying an image layer by a color, and removed by multiplying the image layer by the inverse of the color that was used to create the color cast.
    • The inverse of a color is the same color (same location in XYZ space) in both color spaces, but the channel values are different after the image is converted from some other color space to unbounded sRGB. Hence multiplying by the inverse of a color cast only properly removes that color cast when done in the color space in which the cast was created.

    Hence multiplying an image layer by a given color in one working space can't be undone by multiplying the image layer by the inverse of that color in a different color space.

  2. Multiplying out of gamut colors produces meaningless results. Because color correction depends on multiplication, if any of the image colors or the color cast itself are out of gamut with respect to the bounded sRGB color space, the results of color correcting the out of gamut colors are not just incorrect, but also meaningless. For example, consider the ProPhotoRGB cyan color cast (0.50, 1.00, 1.00). In the unbounded sRGB color space, this color is (-0.017195, 1.114419, 1.004272) and its inverse is (-58.156441, 0.897329, 0.995746). In the unbounded sRGB color space, multiplying the image layer by the wildly out of gamut color (-58.156441, 0.897329, 0.995746) produces entirely meaningless results.

Are such extreme color casts unrealistic?

If the argument is made that such extreme color casts as I show on this page are never encountered in actual image editing, my answer is threefold:

  1. Yes, in fact such extreme color casts are encounted in actual image editing.
  2. Correcting less extreme color casts in the unbounded sRGB color space causes less extreme errors, but nonetheless does cause errors. A smaller error is still an error.
  3. The source of the errors when correcting extreme color casts in the unbounded sRGB color space isn't the extremity of the color casts but rather the fact that the color casts were corrected in the unbounded sRGB color space.

Color correction should be done in the color space in which the color cast was originally created. Unless the color cast was created in the sRGB color space, unbounded sRGB is not the right color space to use to remove the color cast. The fact that unbounded sRGB encodes out of gamut colors using negative channel values makes a bad situation worse: Multiplication is necessary to correct color casts, and multiplying out of gamut colors produces meaningless results.

Appendix 1: Normalizing the RGB channel values before color balancing doesn't work

The suggestion has been made that normalizing RGB channel data allows operations to be correctly performed in the unbounded sRGB color space. Alas normalizing just makes a bad situation worse. After converting the ProPhotoRGB image to unbounded sRGB, normalizing the RGB channel data before attempting to remove the ProPhotoRGB-created cyan color cast ends up producing an image that's mostly red.

Appendix 2: Additional examples of odd colors that result from trying to color balance images in the unbounded sRGB color space

Figures 3 through 6 show what happens to various images with various color casts from various source color spaces, when the images are converted to unbounded sRGB for color correction:

When dealing with camera raw files, it's always better to get the color balance correct during raw processing. But sometimes mistakes are made and sometimes raw files are lost. This photograph of a construction site ought to be very easy to color correct as the predominant colors are close to neutral and the bright clouds along the top edge can be used for color correcting the green color cast:
  • When color-corrected in the AdobeRGB1998 and ProphotoRGB working spaces (images 2 and 3 above, respectively), the resulting images are very similar. The main difference is that in the AdobeRGB image, the pile of dirt at the base of the nearest building is slightly more red, and the grass at the base of the tree is slightly more yellow. So of the two, the image that was color-corrected in the ProPhotoRGB working space seems preferable.
  • When color-corrected in the unbounded sRGB color space (image 4 above), the resulting image has cyan-green grass and dirt, orange clouds, and speckles of orange and cyan all over the Tyvek wrappings on the buildings. I tried using the Tyvek wrapping as a nominal neutral patch, with the same result.
After converting this ProPhotoRGB image to a linear gamma version of the ProPhotoRGB, I gave copies of the image yellow, blue, and cyan color casts. I converted the resulting images to unbounded sRGB for color correction:
  • Correcting the yellow and blue color casts in the unbounded sRGB color space produced believable results that are visually only a little different from the original images.
  • Correcting the cyan color cast in the unbounded sRGB color space turned the purple flowers to an over-saturated shade of blue and turned the green bokeh background to orange.
This crop from Julio Pinar's lovely picture of a young woman reading a book is a Creative Commons BY-SA AdobeRGB image that I converted to a linear gamma version of AdobeRGB1998 and then made two copies, giving the first copy a green color cast and the second copy a red color cast. I converted the resulting images to unbounded sRGB for color correction:
  • Correcting the green color cast in the unbounded sRGB color space made the orange planter and flowers brighter and more saturated, made the purple flowers turn magenta, and made all the green leave and foliage more saturated with a blue-green color cast.
  • Correcting the red color cast in the unbounded sRGB color space made the orange planter a little darker and less saturated and made the grass yellower. A darker, less saturated orange planter won't upset anyone, but correcting the same red color cast in a fair-skinned person's face makes the face look anemic.
This is a picture of some brightly colored paint chip samples, shot raw and interpolated using RawTherapee and the standard dcraw Adobe matrix input profile. I output the image in a linear gamma version of the ProPhotoRGB working space, made six copies, each with a white dot for easy color correction, and gave each copy a fairly extreme color cast (for example, for the cyan color cast I used Levels to lower the Red Channel Output right slider from 100.00 to 50.00). Then I converted the layer stack to unbounded sRGB and attempted to correct the color casts:
  • The band across the middle of each paint chip labelled "incorrect" shows the result of color "correction" in the unbounded sRGB color space.
  • The portion of the paint chip above and below the band labelled "incorrect" shows what the corrected color ought to look like.

Results from using the unbounded sRGB color space to "correct" color casts created in the linear gamma ProPhotoRGB color space range from:

  • Only a little wrong when correcting the yellow color cast (top row left, labelled 1).
  • A little wrong to noticeably wrong when correcting the blue color cast (top row right, labelled 2).
  • Noticeably to very wrong when correcting the red, magenta, and green color casts (red: center row left, labelled 3; magenta: center row right, labelled4; green: bottom row left, labelled 5).
  • Absurdly wrong when correcting cyan color cast (bottom row right, labelled 6): yellow and orange turned green, green turned orange, blue turned pink, purple turned blue and magenta, and so on. Also, the white background is sprinkled with cyan dots (look between the blocks).