我选择了一个使用setPixel()
创建噪音地图的项目。此应用程序的大部分运行时都在setPixel()
函数中使用,因此我希望提高函数执行的速度。
我对此做了一些研究,发现这个:
int index = x + (y * Width);
int col = color.ToArgb();
if (this.Bits == null)
{
this.Bits = new Int32[Width * Height];
}
Bits[index] = col;
建议将作为一种更快捷的方法。但是,这会产生一个完全黑色的图像。
我不完全理解图像处理和内存指针如何工作以完全理解代码并将其重构为更好的代码。
以下是此人实施之前的原始代码:
unsafe
{
var scan0 = (byte*)Iptr;
int bitmapStride = Stride;
int bitmapPixelFormatSize = Depth / 8;
index = (bitmapStride * y) + (x * bitmapPixelFormatSize);
if (bitmapPixelFormatSize == 4)
{
scan0[index + 3] = color.A;
scan0[index + 2] = color.R;
scan0[index + 1] = color.G;
scan0[index] = color.B;
}
else if (bitmapPixelFormatSize == 1)
{
scan0[index] = color.R;
}
else
{
scan0[index + 2] = color.R;
scan0[index + 1] = color.G;
scan0[index] = color.B;
}
}
Iptr
只是一个IntPtr
Stride
是一个int,我唯一可以找到它的地方是Stride = (PixelCount/Height) * (Depth / 8)
x
是宽度
y
是身高
我是否能够得到原始代码块中发生的事情的解释,并且可能有助于理解如何将其转换为更快执行的内容,目前需要大约500,000ms才能完成此功能嵌套为宽度*高度的循环。
答案 0 :(得分:1)
注意:以下信息最初由Bob Powell创建。原始链接(不再有效)是http://bobpowell.net/lockingbits.htm。
我已从https://web.archive.org/web/20120330012542/http://bobpowell.net/lockingbits.htm的互联网档案中复制了此信息。它有点长,但我认为值得保留。
我不确定这是否可以作为您问题的直接答案,但也许它可以帮助您找到解决方案。
使用LockBits方法访问图像数据
通过直接访问像素数据阵列,而不是依赖于GetPixel和SetPixel或其他方法,可以加快许多图像处理任务甚至文件类型转换,例如从每像素32位到每像素8位的转换
你会发现.NET是一个托管代码系统,它通常使用托管数据,所以我们不再经常需要访问存储在内存中的字节,但图像处理是为数不多的当托管数据访问速度太慢时,我们需要再次深入研究查找数据和操作数据的棘手问题。
在我开始讨论这个主题之前,我只是提醒您,用于访问任何非托管数据的方法将根据您编写程序的语言而有所不同。 C#开发人员有机会通过unsafe关键字和指针的使用来直接访问内存中的数据。 Visual Basic程序员应该通过Marshal类方法访问这些数据,这些方法也可能表现出很小的性能损失。
锁定您的位
Bitmap类提供了LockBits和相应的UnlockBits方法,使您可以在内存中修复位图像素数据数组的一部分,直接访问它,最后用修改后的数据替换位图中的位。 LockBits返回一个BitmapData类,用于描述锁定数组中数据的布局和位置。
BitmapData类包含以下重要属性;
Scan0和Stride与内存中数组的关系如图1所示。
Stride属性(如图1所示)保存一行的宽度(以字节为单位)。然而,行的大小可能不是像素大小的精确倍数,因为为了提高效率,系统确保将数据打包成以四字节边界开始并填充为四个字节的倍数的行。这意味着例如每像素宽24像素的图像将具有52的步幅。每行中使用的数据将占用3 * 17 = 51字节,并且1字节的填充将每行扩展为52字节或13 * 4字节。一个宽度为17像素的4BppIndexed图像的步幅为12.其中9个字节,或者更恰当的是8个半字节,将包含数据,并且该行将用另外3个字节填充到4字节边界。
如上所述,该行的数据承载部分根据像素格式布局。包含RGB数据的每像素24位图像将每3个字节具有一个新像素,每四个字节具有32位/像素RGBA。每个字节包含多个像素的像素格式,例如每个像素索引为4位,每个像素索引为1位,必须小心处理,以便所需的像素不会与其相同的邻近像素混淆字节。
找到正确的字节。
因为步幅是行的宽度,要索引任何给定的行或Y坐标,您可以将步幅乘以Y坐标以获取特定行的开头。在行内找到正确的像素可能更困难,并且取决于知道像素格式的布局。以下示例显示如何访问给定像素格式的特定像素。
迭代像素
对于每像素具有一个或多个字节的像素格式,公式很简单,可以通过按顺序循环遍历所有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代码使用编组类
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
方法,可以显着提升图像处理和图像格式转换例程。