当对位图的颜色数据执行顺序操作时,Bitmap.LockBits 方法可以提供巨大的性能提升,因为位图数据只需加载到内存中一次,而不是顺序 GetPixel/SetPixel 调用:每次调用将在内存中加载部分位图数据,然后将其丢弃,以在再次调用这些方法时重复该过程。
如果需要对 GetPixel/SetPixel 进行一次调用,则这些方法可能比 Bitmap.LockBits() 具有性能优势。但是,在这种情况下,在实践中,性能并不是一个因素。
Bitmap.LockBits() 的工作原理:
这是函数调用:
public BitmapData LockBits (Rectangle rect, ImageLockMode flags, PixelFormat format);
// VB.Net
Public LockBits (rect As Rectangle, flags As ImageLockMode, format As PixelFormat) As BitmapData
rect As Rectangle:该参数指定我们感兴趣的Bitmap数据部分;本节的字节将被加载到内存中。它可以是位图的整个大小,也可以是其中的一小部分。
flags As ImageLockMode:指定要执行的锁定类型。对内存的访问可以限制为读或写,或允许并发读/写操作。
它还可用于指定 - 设置 ImageLockMode.UserInputBuffer - BitmapData 对象由调用代码提供。
BitmapData 对象定义了一些 Bitmap 属性(Bitmap 的Width 和Height,扫描线的宽度(Stride:数量组成单行像素的字节,由 Bitmap.Width 乘以每个像素的字节数,四舍五入到 4 字节边界。请参阅有关 Stride 的注释)。
BitmapData.Scan0 属性是指向存储位图数据的初始内存位置的指针 (IntPtr)。
此属性允许指定已存储预先存在的位图数据缓冲区的内存位置。当使用指针在进程之间交换位图数据时,它变得很有用。
请注意,关于 ImageLockMode.UserInputBuffer 的 MSDN 文档令人困惑(如果没有错的话)。
-
format As PixelFormat:用于描述单个像素颜色的格式。实际上,它转换为用于表示颜色的字节数。
当PixelFormat = Format24bppRgb 时,每种颜色由 3 个字节(RGB 值)表示。使用PixelFormat.Format32bppArgb,每种颜色由 4 个字节(RGB 值 + Alpha)表示。
索引格式,如Format8bppIndexed,指定每个字节值是Palette 条目的索引。 Palette 是位图信息的一部分,除非像素格式为 PixelFormat.Indexed:在这种情况下,每个值都是系统颜色表中的一个条目。
新位图对象的默认PixelFormat(如果未指定)为PixelFormat.Format32bppArgb,或PixelFormat.Canonical。
关于 Stride 的重要说明:
如前所述,Stride(也称为扫描线)表示构成单行像素的字节数。由于硬件对齐要求,它总是四舍五入到 4 字节边界(4 的整数倍)。
Stride = [Bitmap Width] * [bytes per Color]
Stride += (Stride Mod 4) * [bytes per Color]
这就是为什么我们总是使用使用PixelFormat.Format32bppArgb 创建的位图的原因之一:位图的Stride 始终已经与所需的边界对齐。
如果位图的格式改为 PixelFormat.Format24bppRgb (每种颜色 3 个字节)?
如果位图的Width 乘以每像素字节数不是4 的倍数,则Stride 将用0s 填充以填补空白。
大小为(100 x 100) 的位图在 32 位和 24 位格式中都没有填充:
100 * 3 = 300 : 300 Mod 4 = 0 : Stride = 300
100 * 4 = 400 : 400 Mod 4 = 0 : Stride = 400
大小为(99 x 100)的位图会有所不同:
99 * 3 = 297 : 297 Mod 4 = 1 : Stride = 297 + ((297 Mod 4) * 3) = 300
99 * 4 = 396 : 396 Mod 4 = 0 : Stride = 396
24 位位图的Stride 被填充添加 3 个字节(设置为0)以填充边界。
当我们检查/修改通过坐标访问单个像素的内部值时,这不是一个问题,类似于 SetPixel/GetPixel 的操作方式:始终可以正确找到像素的位置。
假设我们需要在大小为(99 x 100) 的位图中检查/更改位置(98, 70) 的像素。
仅考虑每个像素的字节数。 Buffer内的像素位置为:
[Bitmap] = new Bitmap(99, 100, PixelFormat = Format24bppRgb)
[Bytes x pixel] = Image.GetPixelFormatSize([Bitmap].PixelFormat) / 8
[Pixel] = new Point(98, 70)
[Pixel Position] = ([Pixel].Y * [BitmapData.Stride]) + ([Pixel].X * [Bytes x pixel])
[Color] = Color.FromArgb([Pixel Position] + 2, [Pixel Position] + 1, [Pixel Position])
将像素的垂直位置乘以扫描线的宽度,缓冲区内的位置将始终正确:计算中包含填充大小。
下一个位置的像素颜色,(0, 71),将返回预期的结果:
顺序读取颜色字节时会有所不同。
第一个扫描行将返回直到最后一个像素(最后 3 个字节)的有效结果:接下来的 3 个字节将返回用于舍入Stride 的字节值,全部设置为0。
这也可能不是问题。例如,应用一个过滤器,每个表示像素的字节序列都会被读取并使用过滤器矩阵的值进行修改:我们只需修改一个 3 个字节的序列,在渲染位图时不会考虑这些字节序列。
但如果我们正在搜索特定的像素序列,这确实很重要:读取不存在的像素颜色可能会影响结果和/或使算法失衡。
对位图的颜色执行统计分析时也是如此。
当然,我们可以在循环中添加一个检查:if [Position] Mod [BitmapData].Width = 0 : continue。
但这会为每次迭代增加一个新的计算。
实际操作
简单的解决方案(更常见的一种)是创建一个格式为PixelFormat.Format32bppArgb 的新位图,因此Stride 将始终正确对齐:
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices
Private Function CopyTo32BitArgb(image As Image) As Bitmap
Dim imageCopy As New Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb)
imageCopy.SetResolution(image.HorizontalResolution, image.VerticalResolution)
For Each propItem As PropertyItem In image.PropertyItems
imageCopy.SetPropertyItem(propItem)
Next
Using g As Graphics = Graphics.FromImage(imageCopy)
g.DrawImage(image,
New Rectangle(0, 0, imageCopy.Width, imageCopy.Height),
New Rectangle(0, 0, image.Width, image.Height),
GraphicsUnit.Pixel)
g.Flush()
End Using
Return imageCopy
End Function
这会生成具有相同 DPI 定义的字节兼容位图; Image.PropertyItems 也是从源图像中复制的。
为了测试它,让我们对图像应用棕褐色调滤镜,使用它的副本来执行位图数据所需的所有修改:
Public Function BitmapFilterSepia(source As Image) As Bitmap
Dim imageCopy As Bitmap = CopyTo32BitArgb(source)
Dim imageData As BitmapData = imageCopy.LockBits(New Rectangle(0, 0, source.Width, source.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb)
Dim buffer As Byte() = New Byte(Math.Abs(imageData.Stride) * imageCopy.Height - 1) {}
Marshal.Copy(imageData.Scan0, buffer, 0, buffer.Length)
Dim bytesPerPixel = Image.GetPixelFormatSize(source.PixelFormat) \ 8;
Dim red As Single = 0, green As Single = 0, blue As Single = 0
Dim pos As Integer = 0
While pos < buffer.Length
Dim color As Color = Color.FromArgb(BitConverter.ToInt32(buffer, pos))
' Dim h = color.GetHue()
' Dim s = color.GetSaturation()
' Dim l = color.GetBrightness()
red = buffer(pos) * 0.189F + buffer(pos + 1) * 0.769F + buffer(pos + 2) * 0.393F
green = buffer(pos) * 0.168F + buffer(pos + 1) * 0.686F + buffer(pos + 2) * 0.349F
blue = buffer(pos) * 0.131F + buffer(pos + 1) * 0.534F + buffer(pos + 2) * 0.272F
buffer(pos + 2) = CType(Math.Min(Byte.MaxValue, red), Byte)
buffer(pos + 1) = CType(Math.Min(Byte.MaxValue, green), Byte)
buffer(pos) = CType(Math.Min(Byte.MaxValue, blue), Byte)
pos += bytesPerPixel
End While
Marshal.Copy(buffer, 0, imageData.Scan0, buffer.Length)
imageCopy.UnlockBits(imageData)
imageData = Nothing
Return imageCopy
End Function
Bitmap.LockBits 不一定是可用的最佳选择。
使用ColorMatrix 类也可以很容易地执行应用过滤器的相同过程,它允许将5x5 矩阵变换应用于位图,只使用一个简单的浮点数组(Single) 值。
例如,让我们使用ColorMatrix 类和众所周知的5x5 矩阵应用灰度过滤器:
Public Function BitmapMatrixFilterGreyscale(source As Image) As Bitmap
' A copy of the original is not needed but maybe desirable anyway
' Dim imageCopy As Bitmap = CopyTo32BitArgb(source)
Dim filteredImage = New Bitmap(source.Width, source.Height, source.PixelFormat)
filteredImage.SetResolution(source.HorizontalResolution, source.VerticalResolution)
Dim grayscaleMatrix As New ColorMatrix(New Single()() {
New Single() {0.2126F, 0.2126F, 0.2126F, 0, 0},
New Single() {0.7152F, 0.7152F, 0.7152F, 0, 0},
New Single() {0.0722F, 0.0722F, 0.0722F, 0, 0},
New Single() {0, 0, 0, 1, 0},
New Single() {0, 0, 0, 0, 1}
})
Using g As Graphics = Graphics.FromImage(filteredImage), attributes = New ImageAttributes()
attributes.SetColorMatrix(grayscaleMatrix)
g.DrawImage(source, New Rectangle(0, 0, source.Width, source.Height),
0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes)
End Using
Return filteredImage
End Function