Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
What is the gamma encoding and why are my color computations wrong?
#1
Lightbulb 
So, you tried to predict or emulate Gimp's behavior with math and failed. And you wonder why. You have come to the right place, take a seat and fasten you seat belt. Also, make sure you check all images at their actual size (click on them to see them in a separate tab, and make sure your browser is exactly 100%zoom) otherwise some example could seem to demonstrated exactly the opposite of what is stated in the text.


The root cause is that our eyes are not linear devices. If you double the power output (for instance, switch on a second light), you don't perceive the room twice as bright. Somewhat brighter, yes, but certainly not twice as bright. Furthermore, our eyes are more sensitive to small differences between dark tones that between light ones.

So, when pixel color values are encoded on a byte, encoding the physical output directly (which is what Gimp calls Linear light) wastes precious bytes values encoding bright nuances we can't perceive, and leaves a handful of values to encode all the subtle darkness nuances we can distinguish.

To avoid this, the values are encoded using a power law that has two nice advantages:
  • The perceived "medium gray" is about half-scale
  • There are many more values to encode low luminosity levels
The theoretical law is encoding = luminosity ** 2.2 (where a luminosity of 1.0 is maximum brightness) and then scaled to the [0 .. 255] range. So for instance physical medium gray (0.5) becomes (0.5 ** (1/2.2) ) * 255 = 186 (#BA) (the 2.2 value is what is known as the "gamma").

In practice images are encoded using a slightly enhanced law called the sRGB color space where the "physical" medium gray is #BC

   

So far, so good. So, where is the problem?

The problem is that if the gamma-encoded values are a good way to store the values, they cannot be used directly to compute things. For instance, the average color of an area with an equal number of black (#00) and white (#FF) pixels is not #80. This area emits towards your eyes half the power of the fully white area, so its average color is the "physical" medium gray, not the "perceived" medium gray as demonstrated by the picture below (that should be checked at full scale(*)):

   

Which side is closer to the middle? In fact, making the square on the right indistinguishable from the one in the middle is a good way to tune your display...

The rule is: when doing computations that are based on physical models (spoiler alert: most are), channel values should always be converted to physical values (Gimp's Linear light) before any math is applied to them.

This applies equally to the three color channels.  For instance if you do a red-green gradient, you would expect the midpoint of the gradient to be orange.
  • In Gimp 2.10, it is so, because the middle of the gradient is where half the power is red and half the power is green. In other words, each color channel emits half the full scale, so the middle color is #BCBC00:    
  • In Gimp 2.8, working directly on the gamma values, the middle color is #808000, which is a much darker brown:      .
As an exercise for the reader, here is the averaging of a pattern with three colors. Click for full scale display otherwise your browser is showing you the wrong colors!

   

If you use Filters > Blur > Pixellize on the pattern, you should obtain the same color.

All these examples show that the direct  computation in the sRGB space produces results that are too dark.

Some more notes:
  • Gamma-encoding is mostly used in low-precision modes (8-bit (from most image formats) and 16-bit integer (from PNG)
  • Use of sRGB of course assumes that the images uses sRGB, if you load an image with another color profile, you have to use that color profile for the conversions.
  • If you use the Pointer dialog or the Sample Points dialog you can ask for the Pixel representation, which in high-precision images is the actual [0.0 .. 1.0] linear light value.
Spreadsheet: 
.zip   GammaSpreadSheet.zip (Size: 126.67 KB / Downloads: 157)
  • ODS format for maximum compatibility  Big Grin  
  • Contains macros, so macros have to be enabled
  • Shows formulas to convert from sRGB to linear and vice-versa
  • Also contains functions for the same (LINEAR2GAMMA and GAMMA2LINEAR) that take [0 .. 1] values and return [0 .. 1] values (in other words you still have to scale to from [0 .. 255] values, but as  a bonus it works with 16-bit values too...)
Python functions for same:
Code:
import sys,math

def srgbToLinear(v255):
    v=v255/255.
    return v/12.92 if v <= 0.04045 else math.pow((v+0.055)/1.055,2.4)

def linearToSrgb(linear):
    srgb=linear*12.92 if linear < 0.0031308 else 1.055*math.pow(linear,1/2.4)-0.055
    return 255*srgb

(*) Browsers scale images using the sRGB values directly, so scaled images may end up darker than the original image. The three-color average example above is a good example. At native size the linear average (right square) is identical to the pattern at the top, but if the image is scaled by your browser (just zoom in/out) your browser will gamma-average the pattern at some point and make it look like the bottom left square.
Reply
#2
Going next level... how is opacity/transparency handled?

The usual Normal more is what is known as the over operator in computer graphics. The general formula or pixel A over pixel B is:

 alphaOut = alphaA + alphaB * (1 - alphaA)
channelOut = ( channelA * alphaA + channelB * alphaB * (1 - alphaA) ) / alphaOut


In the frequent case where the bottom is fullly opaque (alphaB = 1), this simplifies to:

   alphaOut = 1
 channelOut = channelA * alphaA + channel B * (1 - alphaA)


Of course Gimp uses these formulas, but the question is, what are the actual input values... and the answer is, it depends Big Grin 

The great distinction is between Default and Legacy blend modes:

In Default mode:
  • The alpha is the product of the opacity slider (50% opacity: 0.50) and the linear value of the mask. In other words, to achieve the same opacity as a 50% opacity slider, the mask should be set to 188 (#BC) to obtain a linear value of 50% (50.2% in practice).
  • The channel inputs are the linear values (and of course the output is converted back to sRGB)
In Legacy mode(*):
  • The alpha is the product of the opacity slider (50% opacity: 0.50) and the linear value of the mask, like in standard mode. So a 50% opacity is still obtained with a mask value of 188 (#BC).
  • The channel inputs are the sRGB  values.
Which gives:

The computations:
   
The results in Default mode:
   
The results in Legacy mode (where the outcome is the average of the two layers)
   

Augmented spreadsheet with three macros/functions (all inputs and outputs are assumed "linear"):
.zip   GammaAndCompositingSpreadsheet.zip (Size: 118.16 KB / Downloads: 97)
  • OverOpaqueChannel(topValue,topAlpha,bottomValue): computes the result of compositing a pixel over another fully opaque pixel
  • OverAlpha(topAlpha,bottomAlpha): computes the resulting alpha of two pixels
  • OverChannel(topValue,topAlpha,bottomValue,bottomAlpha): computes the resulting color of two pixels
(*) For the curious, "Legacy" is a bit of a misnomer, because if Gimp indeed works on the raw sRGB data for the layer, it still converts the mask to a linear value. However, if you have the curiosity to save the file to 2.6 XCF and open it in 2.8, you expect something different since Gimp would use the direct mask values... but you get the very same visible result! But if you look closer, you find that the mask has been altered, and the 128 and 188 values in the example above have ben converted to 55 and 128, that are their linear values...
Reply
#3
A lot of work in it you did, an absolute great job!

(07-16-2024, 07:25 PM)Ofnuts Wrote: luminosity ** 2.2
(0.5 ** (1/2.2) ) * 255 =

Just a POV about the "**", I would suggest to use ^ instead of ** as the later is mostly known by programmer, which most of us are not, and GIMP does not know it either  Big Grin

   

   
Patrice
Reply
#4
The real exponentiation notation is superscript, everything else is conventions. And most questions I answered about this so far have been from programmers and they mostly use **.
Reply
#5
Just for fun, another demo of your browser improperly scaling on gamma values. Zoom the image below to see it turn red.

   

But if you import it in Gimp and zoom in Gimp (or scale it...) its color won't change.
Reply
#6
Added a post on transparency handling. For ease of reference, I moved it to the beginning of the thread.
Reply


Forum Jump: