注意: 以下信息最初由 Bob Powell 创建。原始链接不再起作用,因此我从位于https://web.archive.org/web/20120330012542/http://bobpowell.net/lockingbits.htm的 Internet 存档中复制了此信息。它有点长,但我认为它值得保留。
我不确定这是否可以直接回答您的问题,但也许它会帮助您找到解决方案。
使用 LockBits 方法访问图像数据
许多图像处理任务甚至文件类型转换,比如从每像素 32 位到每像素 8 位,都可以通过直接访问像素数据数组来加速,而不是依赖 GetPixel 和 SetPixel 或其他方法。
您会知道 .NET 是一个托管代码系统,它最常使用托管数据,因此我们不再需要访问存储在内存中的字节,但是,图像操作是托管数据访问的少数情况之一太慢了,所以我们需要再次深入研究查找数据和操作数据的棘手问题。
在我开始讨论这个主题之前,我只是提醒您,用于访问任何非托管数据的方法将根据您编写程序的语言而有所不同。C# 开发人员有机会通过 unsafe 关键字和指针的使用直接访问内存中的数据。Visual Basic 程序员应该通过 Marshal 类方法访问这些数据,这也可能会显示出小的性能损失。
锁定你的位
Bitmap 类提供 LockBits 和相应的 UnlockBits 方法,使您能够在内存中固定位图像素数据数组的一部分,直接访问它,最后用修改后的数据替换位图中的位。LockBits 返回一个 BitmapData 类,该类描述数据在锁定数组中的布局和位置。
BitmapData 类包含以下重要属性;
- Scan0固定数据数组在内存中的地址
- 步幅单行像素数据的宽度(以字节为单位)。这个宽度是图像像素尺寸的倍数,或者可能是子倍数,并且可以被填充以包含更多字节。我会很快解释为什么。
- PixelFormat数据的实际像素格式。这对于找到正确的字节很重要
- 宽度锁定图像的宽度
- 高度锁定图像的高度
Scan0 和 Stride 与内存中数组的关系如图 1 所示。

如图 1 所示,Stride 属性保存一行的宽度(以字节为单位)。然而,行的大小可能不是像素大小的精确倍数,因为为了提高效率,系统会确保将数据打包到以四字节边界开始并填充为四字节的倍数的行中。这意味着例如,17 像素宽的每像素 24 位图像的步长为 52。每行中使用的数据将占用 3 17 = 51 个字节,1 字节的填充将每行扩展为 52 个字节或 13 4字节。一个 17 像素宽的 4BppIndexed 图像的步幅为 12。其中 9 个字节,或者更准确地说是 8 个半字节,将包含数据,并且该行将用另外 3 个字节填充到 4 字节边界。
如上所述,行的数据承载部分是根据像素格式布局的。包含 RGB 数据的每像素 24 位图像每 3 个字节有一个新像素,每 4 个字节每像素 32 位 RGBA。每个字节包含一个以上像素的像素格式,例如每像素索引 4 位和索引每像素 1 位,必须仔细处理,以免所需像素与同一字节中的相邻像素混淆。
找到正确的字节。
因为步幅是行的宽度,所以要索引任何给定的行或 Y 坐标,您可以将步幅乘以 Y 坐标以获得特定行的开头。在行中找到正确的像素可能更困难,并且取决于了解像素格式的布局。以下示例显示了如何访问给定像素格式的特定像素。
- Format32BppArgb给定X和Y坐标,像素中第一个元素的地址是Scan0+(y * stride)+(x*4)。这指向蓝色字节。以下三个字节包含绿色、红色和 alpha 字节。
- Format24BppRgb给定X和Y坐标,像素中第一个元素的地址是Scan0+(y Stride)+(x 3)。这指向蓝色字节,后面是绿色和红色。
- Format8BppIndexed给定 X 和 Y 坐标,字节的地址是 Scan0+(y*Stride)+x。该字节是图像调色板的索引。
- Format4BppIndexed给定 X 和 Y 坐标,包含像素数据的字节计算为 Scan0+(y*Stride)+(x/2)。对应的字节包含两个像素,上半字节是最左边的,下半字节是两个像素中最右边的。上下半字节的四位用于从 16 色调色板中选择颜色。
- Format1BppIndexed给定 X 和 Y 坐标,包含像素的字节由 Scan0+(y*Stride)+(x/8) 计算。该字节包含 8 位,每个位是一个像素,最左边的像素在第 8 位,最右边的像素在第 0 位。这些位从两个入口调色板中选择。
遍历像素
对于每个像素一个或多个字节的像素格式,公式很简单,可以通过依次循环所有 Y 和 X 值来完成。以下清单中的代码将每像素 32 位图像的蓝色分量设置为 255。在这两种情况下,bm 都是先前创建的位图。
BitmapData bmd=bm.LockBits(new Rectangle(0, 0, 10, 10), System.Drawing.Imaging.ImageLockMode.ReadOnly, bm.PixelFormat);
int PixelSize=4;
for(int y=0; y<bmd.Height; y++)
{
byte* row = (byte *)bmd.Scan0+(y*bmd.Stride);
for(int x = 0; x<bmd.Width; x++)
{
row[x * PixelSize] = 255;
}
}
在 VB 中,此操作的处理方式略有不同,因为 VB 不了解指针,并且需要使用 marshal 类来访问非托管数据。
Dim x As Integer
Dim y As Integer
Dim PixelSize As Integer = 4
Dim bmd As BitmapData = bm.LockBits(new Rectangle(0, 0, 10, 10), System.Drawing.Imaging.ImageLockMode.ReadOnly, bm.PixelFormat)
For y = 0 To bmd.Height - 1
For x = 0 To bmd.Width - 1
Marshal.WriteByte(bmd.Scan0, (bmd.Stride * y) + (4 * x) , 255)
Next
Next
子字节像素。
前面提到的 Format4BppIndexed 和 Format1BppIndexed 像素格式都在一个字节中存储了多个像素。在这种情况下,您需要确保更改一个像素的数据不会影响该字节中保存的其他像素或像素。
索引每像素 1 位图像的方法依赖于使用按位逻辑运算And
并Or
重置或设置字节中的特定位。在对每像素 1 位图像使用上面所示的公式后,X 坐标的低 3 位用于选择所需的位。下面的清单在 C# 和 VB 中显示了这个过程。在这两个示例bmd
中,都是从每像素 1 位图像中提取的位图数据。
C# 代码使用指针并且需要使用不安全的代码进行编译
byte* p=(byte*)bmd.Scan0.ToPointer();
int index=y*bmd.Stride+(x>>3);
byte mask=(byte)(0x80>>(x&0x7));
if(pixel)
p[index]|=mask;
else
p[index]&=(byte)(mask^0xff);
VB代码使用marshal类
Dim mask As Byte = 128 >> (x And 7)
Dim offset As Integer = (y * bmd.Stride) + (x >> 3)
Dim currentPixel As Byte = Marshal.ReadByte(bmd.Scan0, offset)
If pixel = True Then
Marshal.WriteByte(bmd.Scan0, offset, currentPixel Or mask)
Else
Marshal.WriteByte(bmd.Scan0, offset, CByte(currentPixel And (mask Xor 255)))
End If
请注意,在 C# 代码中使用 Marshal 类是非常有效的。我使用指针是因为它提供了最好的性能。
以类似的方式访问每像素 4 位图像中的单个像素。字节的高半字节和低半字节必须分开处理,改变奇数 X 像素的内容不应影响偶数 X 像素。下面的代码显示了如何在 C# 和 VB 中执行此操作。
C#
int offset = (y * bmd.Stride) + (x >> 1);
byte currentByte = ((byte *)bmd.Scan0)[offset];
if((x&1) == 1)
{
currentByte &= 0xF0;
currentByte |= (byte)(colorIndex & 0x0F);
}
else
{
currentByte &= 0x0F;
currentByte |= (byte)(colorIndex << 4);
}
((byte *)bmd.Scan0)[offset]=currentByte;
VB
Dim offset As Integer = (y * bmd.Stride) + (x >> 1)
Dim currentByte As Byte = Marshal.ReadByte(bmd.Scan0, offset)
If (x And 1) = 1 Then
currentByte = currentByte And &HF0
currentByte = currentByte Or (colorIndex And &HF)
Else
currentByte = currentByte And &HF
currentByte = currentByte Or (colorIndex << 4)
End If
Marshal.WriteByte(bmd.Scan0, offset, currentByte)
使用 LockBits 和 UnlockBits
该LockBits
方法采用一个大小可能与正在处理的图像相同或更小的矩形、一个通常与正在处理的图像相同的 PixelFormat 以及一个指定数据是只读还是只写的 ImageLockMode 值,读写或用户分配的缓冲区。最后一个选项不能在 C# 或 VB 中使用,因为指定用户缓冲区的 LockBits 方法重载未包含在 GDI+ 托管包装器中。
UnlockBits
非常重要的是,当所有操作完成后,BitmapData 会通过该方法放回位图中。下面的代码片段说明了这一点。
Dim bmd As BitmapData = bm.LockBits(New Rectangle(0, 0, 10, 10), ImageLockMode.ReadWrite, bm.PixelFormat)
' do operations here
bm.UnlockBits(bmd)
概括
这几乎涵盖了直接访问最流行和最困难的像素格式的各个方面。使用这些技术而不是GetPixel
和SetPixel
提供的方法Bitmap
将显着提高图像处理和图像格式转换例程的性能。