对 LockBits、BitmapData 和 PixelFormat.Format48bppRgb 感到困惑

问题描述

考虑以下代码

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,Color.Red);
    bmp.SetPixel(1,Color.Lime);
    bmp.SetPixel(2,Color.Blue);
    bmp.SetPixel(0,1,Color.White);
    bmp.SetPixel(1,Color.Gray);
    bmp.SetPixel(2,Color.Black);
    bmp.SetPixel(0,2,Color.Cyan);
    bmp.SetPixel(1,Color.Fuchsia);
    bmp.SetPixel(2,Color.Yellow);

    return bmp;
}

上面的代码应该产生一个 3 x 3 Bitmap 实例,如果放大,它应该是这样的:

3 x 3 bitmap

我发现我可以使用 Bitmap.LockBits 方法“扫描”位图的像素信息。我使用基于 24 位和 32 位的 PixelFormat 值作为 pixelFormat 参数成功地做到了这一点。

但是,当使用 PixelFormat.Format48bppRgb 代替时,我还没有理解如何计算像素信息:

public void test()
{
    using (var bmp = Create3x3Bitmap(PixelFormat.Format48bppRgb))
    {
        var lockRect = new Rectangle(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,size);

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

        bmp.UnlockBits(bitmapData);
    }
}

如果我使用调试器运行上面的代码并在调用 Marshal.copy 后立即中断,我可以看到 scanData 字节数组包含 60 个字节。我的理解是,对于三个 RGB 通道中的每一个,都需要两个字节。这意味着每像素 6 个字节。 y 轴上的每个“行”还有两个额外的未使用字节,在本例中为 3 行。

所以如果我做对了,下面是我应该如何解释第一行数组的内容

array content

现在让我感到困惑的是我应该如何解释每个通道的字节对并将其转换回原始颜色。对于第一个像素(红色),蓝色和绿色通道都会找到一对 0,这是有道理的。我不太确定如何将 0 和 32 作为一对应该意味着“全红”,但环顾网络我开始明白范围是 0 到 8192 而不是 0 到 65535,{ {3}}。

果然,使用 BitConverter.ToUInt16 为那对字节得到了 8192 的值。所以红色像素的结果是有道理的。

然而,对于在灰色像素的每个通道上找到的“232,6”对(上图中的索引 26 到 31),它也给出了 1768。而这就是让我感到困惑的地方。由于灰色的 8 位通道在 0 到 255 范围的中间,我预计会是 4095 或 4096,它在 0 到 8192 之间。?‍♂️

我对表示通道的字节对的理解是否正确?如果是这样,我怎么会得到这些灰色的通道值?

解决方法

Minor nit:你写的:

y 轴上的每个“行”还有两个额外的空像素,在本例中为 3 行。

每行有两个额外的字节,使步幅为 20 个字节,而不是三个 6 字节像素值所需的 18 个字节。这满足 GDI+ 要求,即位图步幅是 4 的倍数。

就问题本身而言......

如评论中所述,问答 Why are RGB values of a PixelFormat.Format48bppRgb image only from 0 to 255? 应该对您有所帮助。事实上,这个陈述至少有一半的答案是你的难题:

GDI+ 只允许颜色通道中的值从 0 到 8192。

这意味着当您将像素设置为例如红色时,其 24 位 RGB 值为 (255,0),通过创建值 (8192,0)。这与每像素 16 位的 (65535,0) 不同。

另一部分是像素通道存储为两字节、小端、16 位值。当您看到一个字节 0 (0x00) 后跟一个字节 32 (0x20) 时,解释它的方法是将第一个字节存储在 ushort 变量中,然后将第二个字节剩下 8 位,并将其与同一变量中的第一个字节组合。例如:

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

或者你可以直接调用 BitConverter.ToUInt16(byte[],int)

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