简体   繁体   中英

Confused about LockBits, BitmapData and PixelFormat.Format48bppRgb

Consider the following code:

public static Bitmap Create3x3Bitmap(PixelFormat pixelFormat)
{
    var bmp = new Bitmap(3, 3, pixelFormat);

    // I know SetPixel does not perform well, this is strictly for learning purposes

    bmp.SetPixel(0, 0, Color.Red);
    bmp.SetPixel(1, 0, Color.Lime);
    bmp.SetPixel(2, 0, Color.Blue);
    bmp.SetPixel(0, 1, Color.White);
    bmp.SetPixel(1, 1, Color.Gray);
    bmp.SetPixel(2, 1, Color.Black);
    bmp.SetPixel(0, 2, Color.Cyan);
    bmp.SetPixel(1, 2, Color.Fuchsia);
    bmp.SetPixel(2, 2, Color.Yellow);

    return bmp;
}

The code above should produce a 3 x 3 Bitmap instance which, if magnified, should look like this:

3 x 3 位图

I figured out that I could "scan" the bitmap's pixels information using the Bitmap.LockBits method. I succeeded doing just that using the 24 and 32 bits based PixelFormat values as the pixelFormat argument.

However, I have yet to understand how to work out pixel informations when PixelFormat.Format48bppRgb is used instead:

public void Test()
{
    using (var bmp = Create3x3Bitmap(PixelFormat.Format48bppRgb))
    {
        var lockRect = new Rectangle(0, 0, bmp.Width, bmp.Height);
        var data = bmp.LockBits(lockRect, ImageLockMode.ReadWrite, bmp.PixelFormat);
        var absStride = Math.Abs(data.Stride); // will be equal to 20
        var size = absStride * data.Height; // will be equal to 60

        byte[] scanData = new byte[size];

        Marshal.Copy(data.Scan0, scanData, 0, size);

        // ...
        // more stuff here, irrelevant for the actual question
        // ...

        bmp.UnlockBits(bitmapData);
    }
}

If I run the code above using the debugger and break right after the call to Marshal.Copy , I can see that the scanData byte array contains 60 bytes. My understanding is that for each of the three RGB channels, two bytes are required. This means 6 bytes per pixel. There's also two additional unused bytes for each "row" on the y-axis, which in this case is 3 rows.

So if I'm getting this right, here is how I should interpret the array's content for the first row:

数组内容

Now what confuses me is how I'm supposed to interpret each channel's pair of bytes and translate that back into the original color. It makes sense that for the first pixel (which is red), a pair of 0s would be found for both the blue and green channel. I wasn't so sure what to make of 0 and 32 as a pair that's supposed to mean "full-on red", but looking around the net I came to understand that the range is 0 to 8192 rather than 0 to 65535, due to a limitation of GDI+ .

And sure enough, using BitConverter.ToUInt16 got me a value of 8192 for that pair of bytes. So the result for the red pixel makes sense.

However , it also gives 1768 for the "232, 6" pairs found on each channel of the gray pixel (indexes 26 to 31 on the image above). And that's where I am confused. Since gray color's 8 bits channels are midway in the range of 0 to 255, I would have expected something like 4095 or 4096, which is midway between 0 to 8192. 🤷‍♂️

Is my understanding on byte pairs representing channels even correct? If so then how come I got these channel values for gray color?

Minor nit: you wrote this:

There's also two additional empty pixels for each "row" on the y-axis, which in this case is 3 rows.

There are two additional bytes for each row, making the stride 20 bytes instead of the 18 bytes that would be needed for three 6-byte pixel values. This fulfills the GDI+ requirement that the bitmap stride be a multiple of 4.

As far as the question itself goes…

As noted in a comment, the Q&A Why are RGB values of a PixelFormat.Format48bppRgb image only from 0 to 255? should be helpful to you. Indeed, this statement has at least half of the answer to your puzzle:

GDI+ allows only values from 0 to 8192 in the color channel.

This means that when you set a pixel to, for example, red, which has a 24-bit RGB value of (255,0,0), this is extended to 48-bits by creating a value of (8192,0,0). This is different from the (65535,0,0) one might expect with 16 bits per pixel.

The other part is that the pixel channels are stored as two-byte, little-endian, 16-bit values. When you see a byte 0 ( 0x00 ), followed by a byte 32 ( 0x20 ), the way to interpret that is to store the first byte in a ushort variable, then shift the second byte left by 8 bits and combine that with the first byte in the same variable. Eg:

byte[] redChannelBytes = { 0x00, 0x20 };
ushort redChannel = redChannelBytes[0] | (redChannelBytes[1] << 8);

Or you can just call BitConverter.ToUInt16(byte[], int)

byte[] redChannelBytes = { 0x00, 0x20 };
ushort redChannel = BitConverter.ToUInt16(redChannelBytes, 0);

Maybe I'm a bit late but I think I can add some useful info to the accepted answer.

Since gray color's 8 bits channels are midway in the range of 0 to 255, I would have expected something like 4095 or 4096, which is midway between 0 to 8192.

It's because the transformation between 'ordinary' and 'wide' pixel formats is not linear. The regular pixel formats (and the Color struct as well) represent colors with gamma correction γ = 2.2, whereas the wide pixel formats have no gamma correction (γ = 1.0).

To get the correct 13bpp level, you can use the following formula:

outputLevel = 8192 * Math.Pow(inputLevel / 255d, 1d / gamma)

which gives you 1770 for inputLevel = 128 and gamma = 0.45 (= 1 / 2.2), which is very close to 1768.

Actually Windows is cheating a bit as you can notice for low inputLevel values: the formula above gives you 0 for input levels < 5, though try SetPixel with 48/64 bpp format and low RGB color values and you can see that the raw RGB components are never zero and they are always different. So using a proper gamma correction may ridiculously cause loss of information when widening the pixel format... That's why (besides performance) I use lookup tables instead when translating colors. The linked source points to the 48-bit color transformations.

To make things even more complicated all these apply only when using GDI+ on Windows. The ReactOS implementation uses a simple linear transformation for wide formats, and also uses the full 16 bit range as opposed to the Windows' 13 bpp range. And in Mono the libgdiplus implementation simply does not support the wide formats at all.

That's why the linked source firstly checks whether to use the lookup tables. It always respects the actual underlying behavior and can also use the full 16bpp range depending on the actual GDI+ implementation. Feel free to use the library if you wish. One of the main motivations was to simplify the complexity and also to provide fast solutions for manipulating bitmaps of any PixelFormat . It also allows to access the actual underlying raw data in a managed way (see the ReadRaw / WriteRaw examples under the last link).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM