Unexpected RGB channel values for gray pixel in 48 bits bitmap

Viewed 96

Following up on this question.

I've managed to extract pixel informations off a bitmap instance using Bitmap.LockBits. PixelFormat is Format48bppRbg which, based on my understanding and Peter's answer on the aforementioned question, should store each pixel's RGB color channels using two bytes for storage. The bytes's combined value should be equal to a number between 0 and 8192, representing an RBG channel intensity value. That value is obtained by passing both bytes to the BitConverter.ToUInt16 method.

So upon extracting pixel informations for the following 3 x 3 bitmap (magnified here for clarity):

bitmap

I'm having first row of pixel channels going like this:

Pixel color Red intensity Green intensity Blue intensity
RED 8192 0 0
GREEN 0 8192 0
BLUE 0 0 8192

So far so good.

On the second row, however, it goes like this:

Pixel color Red intensity Green intensity Blue intensity
WHITE 8192 8192 8192
GRAY (!) 1768 (!) 1768 (!) 1768 (!)
BLACK 0 0 0

The white and black pixel channel values make sense to me.

The gray, however, doesn't.

If you use any color picker on the gray pixel above, you should get a perfectly medium gray. In 24 bits color it should be the equivalent of (R: 128, G: 128, B: 128), or #808080 in hexadecimal form.

Then how come, in a Format48bppRpg pixel format, the channels intensity is way below the expected, middle 4096 value? Isn't this GDI+ based range of 0-8192 supposed to work like its 8 bits counterpart, with 0 being the lowest intensity and 8192 the highest? What am I missing here?

For reference, here is a screen capture from Visual Studio debugger showing the raw bytes indexes and values, with additional notes on the stride, channels positions and their extracted intensity value, up to the gray pixel:

screen capture

1 Answers

The part where you state an incorrect assumption is that #808080 is "perfectly medium gray". It is not, at least not if you look at it from a certain way (more about it here).

Many color standards, including sRGB, use gamma compression to make darker colors more spaced out in the range of 256 values normally used to store RGB colors. This roughly means taking a square root (or 2.2-root) of the relative component value before encoding this value. The reason is that the human eye (like other senses) perceives brightness logarithmically, thus it is important to represent even an arithmetically small change in the brightness if it would mean actually doubling it, for example.

The byte value of 128 is actually about 21,95 % (128/255 ^ 2.2) of full brightness, which is what you're seeing in the case of 16-bit components. The space of possible values there is much larger, thus GDI (or the format) doesn't need to store them in a special way anymore.

In case you need an algorithm, taking the 2.2-root of the value works mostly well, but the correct formula is a bit different, see here. The root function normally has an infinite slope near zero, so the specific formula attempts to fix that by making that portion linear. A piece of code can be derived from that quite easily:

static byte TransformComponent(ushort linear)
{
    const double a = 0.055;
    var c = linear / 8192.0;
    c = c <= 0.0031308 ? 12.92 * c : (1 + a) * Math.Pow(c, 1/2.4) - a;
    return (byte)Math.Round(c * Byte.MaxValue);
}

This gives 128 for the value of 1768.

Related