【问题标题】:Confused about LockBits, BitmapData and PixelFormat.Format48bppRgb对 LockBits、BitmapData 和 PixelFormat.Format48bppRgb 感到困惑
【发布时间】:2021-07-19 20:55:09
【问题描述】:

考虑以下代码:

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;
}

上面的代码应该生成一个 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, 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);
    }
}

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

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

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

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

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

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

【问题讨论】:

标签: c# .net .net-core gdi+


【解决方案1】:

小笨蛋:你写了这个:

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

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

就问题本身而言……

正如评论中所述,Q&A Why are RGB values of a PixelFormat.Format48bppRgb image only from 0 to 255? 应该对您有所帮助。事实上,这句话至少能解答你的疑惑:

GDI+ 只允许在颜色通道中使用 0 到 8192 之间的值。

这意味着当您将像素设置为例如红色时,其 24 位 RGB 值为 (255,0,0),通过创建 (8192, 0,0)。这与人们可能期望的每像素 16 位的 (65535,0,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);

【讨论】:

    【解决方案2】:

    也许我来晚了,但我想我可以在接受的答案中添加一些有用的信息。

    由于灰色的 8 位通道介于 0 到 255 之间,我预计会出现 4095 或 4096 之类的值,介于 0 到 8192 之间。

    这是因为“普通”和“宽”像素格式之间的转换不是线性的。常规像素格式(以及 Color 结构体)表示具有伽马校正 γ = 2.2 的颜色,而宽像素格式没有伽马校正 (γ = 1.0)。

    要获得正确的 13bpp 级别,可以使用以下公式:

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

    对于 inputLevel = 128gamma = 0.45 (= 1 / 2.2),它为您提供 1770,非常接近 1768。

    实际上,Windows 有点作弊,您可以注意到低 inputLevel 值:上面的公式为您提供输入级别 SetPixel 并且您可以看到原始 RGB 分量从不为零,而且它们总是不同的。因此,在扩大像素格式时,使用适当的伽马校正可能会荒谬地导致信息丢失......这就是为什么(除了性能之外)我在翻译颜色时使用use 查找表。链接源指向 48 位颜色转换。

    为了使事情变得更加复杂,所有这些仅适用于在 Windows 上使用 GDI+ 时。 ReactOS implementation 对宽格式使用简单的线性转换,并且还使用完整的 16 位范围,而不是 Windows 的 13 bpp 范围。而在 Mono 中,libgdiplus implementation 根本不支持宽格式。

    这就是为什么链接源首先检查是否使用查找表。它始终尊重实际的底层行为,也可以根据实际的 GDI+ 实现使用完整的 16bpp 范围。如果您愿意,请随意使用该库。主要动机之一是简化复杂性并提供fast 解决方案来处理任何PixelFormat 的位图。它还允许以托管方式访问实际的底层原始数据(请参阅最后一个链接下的 ReadRaw/WriteRaw 示例)。

    【讨论】:

      猜你喜欢
      • 2021-11-28
      • 1970-01-01
      • 2013-03-29
      • 2014-11-02
      • 1970-01-01
      • 2014-03-18
      • 2012-10-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多