c# 设置像素颜色的更快方法
c# faster way of setting pixel colours
我选择了一个使用 setPixel()
创建噪声图的项目。此应用程序的大部分运行时间都花在 setPixel()
函数中,因此我希望提高该函数的执行速度。
我对此做了一些研究,发现:
int index = x + (y * Width);
int col = color.ToArgb();
if (this.Bits == null)
{
this.Bits = new Int32[Width * Height];
}
Bits[index] = col;
已被推荐为更快的方法。但是,这会生成全黑图像。
我不是 100% 理解图像处理和内存指针是如何工作的,以便能够完全理解代码并将其重构为更好的东西。
这是之前实现的人的原始代码:
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
是一个整数,我唯一能找到这个设置的地方是 Stride = (PixelCount/Height) * (Depth / 8)
x
是宽度
y
是身高
我能否得到原始代码块中发生的事情的解释,并可能有助于理解如何将其转换为执行速度更快的东西,目前完成此功能大约需要 500,000 毫秒,因为到宽度 * 高度的嵌套 for 循环。
注:以下信息最初由 Bob Powell 创建。原来的 link 不再起作用,所以我从 https://web.archive.org/web/20120330012542/http://bobpowell.net/lockingbits.htm 的 Internet Archive 复制了这些信息。有点长,但我觉得还是值得收藏的。
我不确定这是否会直接回答您的问题,但也许它会帮助您找到解决方案。
使用 LockBits 方法访问图像数据
许多图像处理任务甚至文件类型转换,比如从 32 bit-per-pixel 到 8 bit-per-pixel 都可以通过直接访问像素数据数组来加速,而不是依赖 GetPixel 和 SetPixel 或其他方法。
您会意识到 .NET 是一个托管代码系统,它最常使用托管数据,因此我们不再需要经常访问存储在内存中的字节,但是,图像处理是少数情况之一托管数据访问速度太慢,因此我们需要再次深入研究查找和操作数据的棘手问题。
在开始讨论当前主题之前,我只想提醒您,用于访问任何非托管数据的方法将因编写程序所用的语言而异。 C# 开发人员有机会通过 unsafe 关键字和指针的使用直接访问内存中的数据。 Visual Basic 程序员应通过 Marshal class 方法访问此类数据,这也可能会造成少量性能损失。
锁定你的位
Bitmap class 提供了LockBits 和对应的UnlockBits 方法,使您可以在内存中固定一部分位图像素数据数组,直接访问它,最后用修改后的数据替换位图中的位. LockBits returns 描述锁定数组中数据的布局和位置的 BitmapData class。
BitmapData class 包含以下重要属性;
- 扫描0固定数据数组在内存中的地址
- 步幅 单行像素数据的宽度,以字节为单位。这个宽度
是像素尺寸的倍数,或者可能 sub-multiple
图像并可能被填充以包含更多字节。恶劣地
稍后解释原因。
- PixelFormat 数据的实际像素格式。这个很重要
找到正确的字节
- 宽度锁定图片的宽度
- 高度锁定图片的高度
Scan0和Stride与内存中数组的关系如图1所示
Stride 属性,如图 1 所示,以字节为单位保存一行的宽度。然而,行的大小可能不是像素大小的精确倍数,因为为了提高效率,系统确保将数据打包到以四字节边界开始的行中,并填充为四字节的倍数。这意味着例如每像素 24 位 17 像素宽的图像的步幅为 52。每行中使用的数据将占用 317 = 51 个字节,并且 1 个字节的填充将扩展每行到 52 个字节或 134 个字节。一个 17 像素宽的 4BppIndexed 图像的步幅为 12。九个字节,或者更准确地说八个半字节,将包含数据,并且该行将用另外 3 个字节填充到 4 字节边界。
如上文所述,行的数据承载部分根据像素格式进行布局。包含 RGB 数据的每像素 24 位图像将每 3 个字节有一个新像素,每四个字节为每像素 32 位 RGBA。每个字节包含一个以上像素的像素格式,例如索引的每个像素 4 位和索引的每个像素 1 位,必须仔细处理,以便所需的像素不会与同一字节中的相邻像素混淆。
找到正确的字节。
因为步幅是一行的宽度,要索引任何给定的行或 Y 坐标,您可以将步幅乘以 Y 坐标以获得特定行的开头。在行中找到正确的像素可能更加困难,并且取决于了解像素格式的布局。以下示例显示了如何访问给定像素格式的特定像素。
- Format32BppArgb给定X和Y坐标,第一个地址
像素中的元素是 Scan0+(y * stride)+(x*4)。这指向
蓝色字节。以下三个字节包含绿色、红色和 alpha
字节。
- Format24BppRgb给定X和Y坐标,第一个的地址
像素中的元素是Scan0+(yStride)+(x3)。这指向
蓝色字节后跟绿色和红色。
- Format8BppIndexed给定X和Y坐标的地址
字节是can0+(y*步幅)+x。这个字节是图像的索引
调色板。
- Format4BppIndexed 给定 X 和 Y 坐标包含
像素数据计算为 Scan0+(y*Stride)+(x/2)。相应的
字节包含两个像素,高半字节是最左边的和
低半字节是两个像素的最右边。的四个位
上下半字节用于 select 来自 16 的颜色
调色板。
- Format1BppIndexed 给定 X 和 Y 坐标,包含的字节
像素由 Scan0+(y*Stride)+(x/8) 计算。该字节包含
8位,每一位是一个像素,最左边的像素在第8位,
位 0 中最右边的像素。来自两个条目的位 select
调色板。
遍历像素
对于每个像素一个或多个字节的像素格式,公式很简单,可以通过按顺序遍历所有 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 class 来访问非托管数据。
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
Sub-byte 像素。
前面提到的Format4BppIndexed和Format1BppIndexed像素格式都是在一个字节中存储了一个以上的像素。在这种情况下,您需要确保更改一个像素的数据不会影响该字节中保存的另一个或多个像素。
索引每像素 1 位图像的方法依赖于使用按位逻辑运算 And
和 Or
来重置或设置字节中的特定位。在使用上面显示的每像素图像 1 位的公式后,X 坐标的低 3 位用于 select 所需的位。下面的清单显示了 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代码使用元帅class
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 class 是非常有效的。我使用指针是因为它提供了最好的性能。
访问每像素 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 值,该值指定是否数据是 read-only、write-only、read-write 或用户分配的缓冲区。最后一个选项不能在 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)
总结
这几乎涵盖了直接访问最流行和最困难的像素格式的各个方面。使用这些技术而不是 Bitmap
提供的 GetPixel
和 SetPixel
方法将显着提高图像处理和图像格式转换例程的性能。
我选择了一个使用 setPixel()
创建噪声图的项目。此应用程序的大部分运行时间都花在 setPixel()
函数中,因此我希望提高该函数的执行速度。
我对此做了一些研究,发现:
int index = x + (y * Width);
int col = color.ToArgb();
if (this.Bits == null)
{
this.Bits = new Int32[Width * Height];
}
Bits[index] = col;
已被推荐为更快的方法。但是,这会生成全黑图像。
我不是 100% 理解图像处理和内存指针是如何工作的,以便能够完全理解代码并将其重构为更好的东西。
这是之前实现的人的原始代码:
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
是一个整数,我唯一能找到这个设置的地方是 Stride = (PixelCount/Height) * (Depth / 8)
x
是宽度
y
是身高
我能否得到原始代码块中发生的事情的解释,并可能有助于理解如何将其转换为执行速度更快的东西,目前完成此功能大约需要 500,000 毫秒,因为到宽度 * 高度的嵌套 for 循环。
注:以下信息最初由 Bob Powell 创建。原来的 link 不再起作用,所以我从 https://web.archive.org/web/20120330012542/http://bobpowell.net/lockingbits.htm 的 Internet Archive 复制了这些信息。有点长,但我觉得还是值得收藏的。
我不确定这是否会直接回答您的问题,但也许它会帮助您找到解决方案。
使用 LockBits 方法访问图像数据
许多图像处理任务甚至文件类型转换,比如从 32 bit-per-pixel 到 8 bit-per-pixel 都可以通过直接访问像素数据数组来加速,而不是依赖 GetPixel 和 SetPixel 或其他方法。
您会意识到 .NET 是一个托管代码系统,它最常使用托管数据,因此我们不再需要经常访问存储在内存中的字节,但是,图像处理是少数情况之一托管数据访问速度太慢,因此我们需要再次深入研究查找和操作数据的棘手问题。
在开始讨论当前主题之前,我只想提醒您,用于访问任何非托管数据的方法将因编写程序所用的语言而异。 C# 开发人员有机会通过 unsafe 关键字和指针的使用直接访问内存中的数据。 Visual Basic 程序员应通过 Marshal class 方法访问此类数据,这也可能会造成少量性能损失。
锁定你的位
Bitmap class 提供了LockBits 和对应的UnlockBits 方法,使您可以在内存中固定一部分位图像素数据数组,直接访问它,最后用修改后的数据替换位图中的位. LockBits returns 描述锁定数组中数据的布局和位置的 BitmapData class。
BitmapData class 包含以下重要属性;
- 扫描0固定数据数组在内存中的地址
- 步幅 单行像素数据的宽度,以字节为单位。这个宽度 是像素尺寸的倍数,或者可能 sub-multiple 图像并可能被填充以包含更多字节。恶劣地 稍后解释原因。
- PixelFormat 数据的实际像素格式。这个很重要 找到正确的字节
- 宽度锁定图片的宽度
- 高度锁定图片的高度
Scan0和Stride与内存中数组的关系如图1所示
Stride 属性,如图 1 所示,以字节为单位保存一行的宽度。然而,行的大小可能不是像素大小的精确倍数,因为为了提高效率,系统确保将数据打包到以四字节边界开始的行中,并填充为四字节的倍数。这意味着例如每像素 24 位 17 像素宽的图像的步幅为 52。每行中使用的数据将占用 317 = 51 个字节,并且 1 个字节的填充将扩展每行到 52 个字节或 134 个字节。一个 17 像素宽的 4BppIndexed 图像的步幅为 12。九个字节,或者更准确地说八个半字节,将包含数据,并且该行将用另外 3 个字节填充到 4 字节边界。
如上文所述,行的数据承载部分根据像素格式进行布局。包含 RGB 数据的每像素 24 位图像将每 3 个字节有一个新像素,每四个字节为每像素 32 位 RGBA。每个字节包含一个以上像素的像素格式,例如索引的每个像素 4 位和索引的每个像素 1 位,必须仔细处理,以便所需的像素不会与同一字节中的相邻像素混淆。
找到正确的字节。
因为步幅是一行的宽度,要索引任何给定的行或 Y 坐标,您可以将步幅乘以 Y 坐标以获得特定行的开头。在行中找到正确的像素可能更加困难,并且取决于了解像素格式的布局。以下示例显示了如何访问给定像素格式的特定像素。
- Format32BppArgb给定X和Y坐标,第一个地址 像素中的元素是 Scan0+(y * stride)+(x*4)。这指向 蓝色字节。以下三个字节包含绿色、红色和 alpha 字节。
- Format24BppRgb给定X和Y坐标,第一个的地址 像素中的元素是Scan0+(yStride)+(x3)。这指向 蓝色字节后跟绿色和红色。
- Format8BppIndexed给定X和Y坐标的地址 字节是can0+(y*步幅)+x。这个字节是图像的索引 调色板。
- Format4BppIndexed 给定 X 和 Y 坐标包含 像素数据计算为 Scan0+(y*Stride)+(x/2)。相应的 字节包含两个像素,高半字节是最左边的和 低半字节是两个像素的最右边。的四个位 上下半字节用于 select 来自 16 的颜色 调色板。
- Format1BppIndexed 给定 X 和 Y 坐标,包含的字节 像素由 Scan0+(y*Stride)+(x/8) 计算。该字节包含 8位,每一位是一个像素,最左边的像素在第8位, 位 0 中最右边的像素。来自两个条目的位 select 调色板。
遍历像素
对于每个像素一个或多个字节的像素格式,公式很简单,可以通过按顺序遍历所有 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 class 来访问非托管数据。
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
Sub-byte 像素。
前面提到的Format4BppIndexed和Format1BppIndexed像素格式都是在一个字节中存储了一个以上的像素。在这种情况下,您需要确保更改一个像素的数据不会影响该字节中保存的另一个或多个像素。
索引每像素 1 位图像的方法依赖于使用按位逻辑运算 And
和 Or
来重置或设置字节中的特定位。在使用上面显示的每像素图像 1 位的公式后,X 坐标的低 3 位用于 select 所需的位。下面的清单显示了 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代码使用元帅class
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 class 是非常有效的。我使用指针是因为它提供了最好的性能。
访问每像素 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 值,该值指定是否数据是 read-only、write-only、read-write 或用户分配的缓冲区。最后一个选项不能在 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)
总结
这几乎涵盖了直接访问最流行和最困难的像素格式的各个方面。使用这些技术而不是 Bitmap
提供的 GetPixel
和 SetPixel
方法将显着提高图像处理和图像格式转换例程的性能。