对 LockBits、BitmapData 和 PixelFormat.Format48bppRgb 感到困惑
Confused about LockBits, BitmapData and 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, 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,due to a limitation of GDI+.
果然,使用 BitConverter.ToUInt16
得到了那对字节的值 8192。所以红色像素的结果是有道理的。
但是,它还为在灰色像素的每个通道上找到的“232, 6”对提供 1768(上图中的索引 26 到 31)。 这就是我感到困惑的地方。由于灰色的 8 位通道在 0 到 255 范围的中间,我本以为是 4095 或 4096,它在 0 到 8192 之间。♂️
我对表示通道的字节对的理解是否正确?如果是这样,那我怎么得到这些灰色通道值?
小傻瓜:你写了这个:
There's also two additional empty pixels for each "row" on the y-axis, which in this case is 3 rows.
每行有两个额外的 字节 ,使步幅为 20 字节,而不是三个 6 字节像素值所需的 18 字节。这满足了 GDI+ 要求位图跨度是 4 的倍数。
就问题本身而言……
如评论所述,问答 应该对您有所帮助。事实上,这句话至少能解答你一半的困惑:
GDI+ allows only values from 0 to 8192 in the color channel.
这意味着当您将像素设置为例如红色时,其 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);
也许我来晚了一点,但我想我可以在已接受的答案中添加一些有用的信息。
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.
这是因为'ordinary'和'wide'像素格式之间的转换不是线性的。常规像素格式(以及 Color
结构)表示具有伽玛校正 γ = 2.2 的颜色,而宽像素格式没有伽玛校正 (γ = 1.0)。
要获得正确的 13bpp 级别,您可以使用以下公式:
outputLevel = 8192 * Math.Pow(inputLevel / 255d, 1d / gamma)
inputLevel = 128
和 gamma = 0.45
(= 1 / 2.2) 为 1770,非常接近 1768。
实际上 Windows 有点作弊,因为您可以注意到较低的 inputLevel
值:上面的公式为输入级别 < 5 提供 0,尽管尝试 SetPixel
和 48/ 64 bpp 格式和低 RGB 颜色值,您可以看到原始 RGB 分量永远不会为零,它们总是不同的。因此,在扩大像素格式时,使用适当的伽马校正可能会荒谬地导致信息丢失……这就是为什么(除了性能之外)我 use 在转换颜色时查找表。 linked 源指向 48 位颜色转换。
为了使事情变得更加复杂,所有这些仅在 Windows 上使用 GDI+ 时适用。 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 根本不支持宽格式。
这就是 linked 源首先检查是否使用查找表的原因。它始终尊重实际的底层行为,并且还可以根据实际的 GDI+ 实现使用完整的 16bpp 范围。如果您愿意,请随意使用图书馆。主要动机之一是简化复杂性,并提供 fast 处理任何 PixelFormat
位图的解决方案。它还允许以托管方式访问实际的底层原始数据(请参阅最后 link 下的 ReadRaw
/WriteRaw
示例)。
考虑以下代码:
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,due to a limitation of GDI+.
果然,使用 BitConverter.ToUInt16
得到了那对字节的值 8192。所以红色像素的结果是有道理的。
但是,它还为在灰色像素的每个通道上找到的“232, 6”对提供 1768(上图中的索引 26 到 31)。 这就是我感到困惑的地方。由于灰色的 8 位通道在 0 到 255 范围的中间,我本以为是 4095 或 4096,它在 0 到 8192 之间。♂️
我对表示通道的字节对的理解是否正确?如果是这样,那我怎么得到这些灰色通道值?
小傻瓜:你写了这个:
There's also two additional empty pixels for each "row" on the y-axis, which in this case is 3 rows.
每行有两个额外的 字节 ,使步幅为 20 字节,而不是三个 6 字节像素值所需的 18 字节。这满足了 GDI+ 要求位图跨度是 4 的倍数。
就问题本身而言……
如评论所述,问答
GDI+ allows only values from 0 to 8192 in the color channel.
这意味着当您将像素设置为例如红色时,其 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);
也许我来晚了一点,但我想我可以在已接受的答案中添加一些有用的信息。
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.
这是因为'ordinary'和'wide'像素格式之间的转换不是线性的。常规像素格式(以及 Color
结构)表示具有伽玛校正 γ = 2.2 的颜色,而宽像素格式没有伽玛校正 (γ = 1.0)。
要获得正确的 13bpp 级别,您可以使用以下公式:
outputLevel = 8192 * Math.Pow(inputLevel / 255d, 1d / gamma)
inputLevel = 128
和 gamma = 0.45
(= 1 / 2.2) 为 1770,非常接近 1768。
实际上 Windows 有点作弊,因为您可以注意到较低的 inputLevel
值:上面的公式为输入级别 < 5 提供 0,尽管尝试 SetPixel
和 48/ 64 bpp 格式和低 RGB 颜色值,您可以看到原始 RGB 分量永远不会为零,它们总是不同的。因此,在扩大像素格式时,使用适当的伽马校正可能会荒谬地导致信息丢失……这就是为什么(除了性能之外)我 use 在转换颜色时查找表。 linked 源指向 48 位颜色转换。
为了使事情变得更加复杂,所有这些仅在 Windows 上使用 GDI+ 时适用。 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 根本不支持宽格式。
这就是 linked 源首先检查是否使用查找表的原因。它始终尊重实际的底层行为,并且还可以根据实际的 GDI+ 实现使用完整的 16bpp 范围。如果您愿意,请随意使用图书馆。主要动机之一是简化复杂性,并提供 fast 处理任何 PixelFormat
位图的解决方案。它还允许以托管方式访问实际的底层原始数据(请参阅最后 link 下的 ReadRaw
/WriteRaw
示例)。