在图像处理中使用指针
如何在 C# 中处理图像时使用指针
引言
使用 Bitmap
类及其方法 GetPixel
、SetPixel
来访问位图像素速度很慢。在本文中,我们将看到如何通过 FastBitmap
类来提高访问位图像素的速度。
正如 C# 中快速图像处理 的一些读者所建议的,我们还将编写 3 个类似的程序并比较它们的性能。这些程序将图像的中心替换为其灰度版本。
- 程序 #1 将使用
FastBitmap
类 - 程序 #2 将使用
Bitmap
类及其方法GetPixel
、SetPixel()
- 程序 #3 将使用 C++ 和 opencv
关于 FastBitmap
FastBitmap
允许访问位图数据的原始内存。为了方便起见,该类实现了 IDisposable
接口,可以轻松管理非托管的原始内存数据。
该类允许处理位图中的一个矩形区域。我们可以使用以下属性来定位此矩形中的像素
YY, XX
是矩形的左上角点Width
,Height
是矩形的尺寸PixelSize
是每个像素的大小(以字节为单位)Scan0
指向矩形左上角像素的位置Stride
是图像像素行以字节为单位的宽度
构造函数锁定位图的原始内存,允许我们通过指针访问它。它接受位图和一个可选的要处理的矩形。Dispose
释放非托管资源并解锁内存 - 完成位图处理后应调用它。
例如
fb = new FastBitmap(bitmap,3,1,4,3);
...
fb.Dispose()
假设位图是 24bpp-BGR 格式的彩色图像,其尺寸为 9x6。
fb.PixelSize
为 3,因为每个像素有 3 个字节。- 我们处理位于
(3,1)
的4x3
矩形,因此fb.Width
=4,fb.Height
=3,fb.XX
=3,fb.YY
=1 fb.Stride
是fb.PixelSize
*位图宽度
,所以fb.Stride=
9*3=27fb.scan0
将指向位于(3,1)
的像素。
要访问矩形中的像素 (xx,yy),我们使用以下公式
Location(xx,yy) = fb.Scan0 + yy * fb.Stride + xx * fb.PixelSize
使用 FastBitmap
下面的演示展示了如何使用这个类 - 该演示将图像的中心替换为其灰度版本。
if (bitmap.PixelFormat != PixelFormat.Format24bppRgb &&
bitmap.PixelFormat != PixelFormat.Format32bppArgb) { // <== Z1
return;
}
int ww = bitmap.Width / 8;
int hh = bitmap.Height / 8;
using (FastBitmap fbitmap = new FastBitmap(bitmap, ww, hh,
bitmap.Width - 2 * ww, bitmap.Height - 2 * hh)) { // <== Z2
unsafe { // <== Z3
byte* row = (byte*)fbitmap.Scan0, bb = row; // <== Z4
for ( int yy = 0; yy < fbitmap.Height; yy++,
bb = (row += fbitmap.Stride)) { // <== Z5
for (int xx = 0; xx < fbitmap.Width ; xx++,
bb += fbitmap.PixelSize) { // <== Z6
// *(bb + 0) is B (Blue ) component of the pixel
// *(bb + 1) is G (Green) component of the pixel
// *(bb + 2) is R (Red ) component of the pixel
// *(bb + 3) is A (Alpha) component of the pixel ( for 32bpp )
byte gray = (byte)((1140 * *(bb + 0) +
5870 * *(bb + 1) +
2989 * *(bb + 2)) / 10000); // <== Z7
*(bb + 0) = *(bb + 1) = *(bb + 2) = gray;
}
}
}
}
在 Z1
中,我们检查 bitmap
格式是否为 24bpp-BGR 或 32bpp-BGRA 格式的彩色图像。
- 在 24bpp-BGR 格式中,每个像素存储 24 位(3 字节)每像素。每个像素分量存储 8 位(1 字节),顺序如下:
- 像素的
B
分量存储在字节 0 中(蓝色) - 像素的
G
分量存储在字节 1 中(绿色) - 像素的
R
分量存储在字节 2 中(红色)
- 像素的
- 在 32bpp-BGR 格式中,每个像素存储 32 位(4 字节)每像素。每个像素分量存储 8 位(1 字节),顺序如下:
- 像素的
B
分量存储在字节 0 中(蓝色) - 像素的
G
分量存储在字节 1 中(绿色) - 像素的
R
分量存储在字节 2 中(红色) - 像素的
A
分量存储在字节 3 中(Alpha)
- 像素的
我们可以看到 32bpp-BGRA 格式在 24bpp-BGR 格式的基础上增加了一个 alpha 分量。我们可以利用这一观察结果来用相同的代码处理这些图像。
在 Z2
中,我们正在使用 using
块。此块确保在块结束时调用在块开始时创建的 FastBitmap
对象的 Dispose
方法。在这种情况下,矩形的宽度和高度是位图的 3/4,其左上角像素位于(位图宽度的 1/8,位图高度的 1/8)。
在 Z3
中,我们开始一个 unsafe
块,它允许我们在其中使用指针。请注意,我们还需要使用 unsafe
编译器标志进行编译。
在 Z4
中,我们声明了两个字节指针。这些指针将用于逐像素访问像素。
row
指向当前行的第一个像素bb
指向当前像素。
在 Z5
中,我们逐行(从上到下)循环,并将 row
指针更新为指向当前行的第一个像素(注意 Stride
属性的使用)。
在 Z6
中,我们逐像素循环遍历当前行,并更新 bb
指针(注意 PixelSize
属性的使用)。
在 Z7
中,我们使用 bb
当前指向的像素来处理像素。
在此演示中,我们将像素的颜色转换为灰度颜色,方法是将所有像素的分量设置为以下公式的值:
- 0.1140 * (蓝色分量) + 0.5870 * (绿色分量) + 0.2989 * (红色分量)
让我们看看演示如何处理左图并生成右图。
基准测试
读者建议将性能与其他的图像处理方法进行比较。
Bitmap GetPixel 和 SetPixel 方法
该演示与原始演示程序类似。但是,我们将使用 Bitmap
类的 SetPixel
和 GetPixel
方法,而不是指针。
if (bitmap.PixelFormat != PixelFormat.Format24bppRgb &&
bitmap.PixelFormat != PixelFormat.Format32bppArgb) {
return;
}
int w0 = bitmap.Width / 8;
int h0 = bitmap.Height / 8;
int x1 = w0;
int y1 = h0;
int xn = x1 + bitmap.Width - 2 * w0;
int yn = y1 + bitmap.Height - 2 * h0;
Color gray, cc;
for ( int yy = y1; yy < yn; yy++) { // <== Z1
for (int xx = x1; xx < xn; xx++) {
cc = bitmap.GetPixel(xx, yy); // <== Z2
byte gg = (byte)((cc.B * 1140 +
cc.G * 5870 +
cc.R * 2989) / 10000); // <== Z3
gray = Color.FromArgb( gg,gg,gg);
bitmap.SetPixel(xx, yy, gray); // <== Z4
}
}
- 在
Z1
中,我们逐像素(从上到下,从左到右)遍历矩形的所有像素位置。 - 在
Z2
中,我们使用GetPixel
方法获取当前像素的Color
。 - 在
Z3
中,我们通过将所有分量设置为上述灰度公式来创建灰度Color
。 - 在
Z4
中,我们使用SetPixel
方法将像素设置为在上一步中找到的灰度Color
。
C++ 和 opencv 方法
该演示与原始演示程序类似,但我们将使用 C++ 和 opencv 库,而不是指针。我试图遵循 FastBitmap
类的思路。
Mat bitmap = imread(argv[1], CV_LOAD_IMAGE_COLOR); // <== Z1
...
int ww = bitmap.cols - 2 * bitmap.cols / 8; // <== Z2
int hh = bitmap.rows - 2 * bitmap.rows / 8;
int x1 = bitmap.cols / 8;
int y1 = bitmap.rows / 8;
int pixelSize = bitmap.channels(); // <== Z3
int stride = pixelSize * bitmap.cols;
uchar* scan0 = bitmap.ptr<uchar>(0) + (y1 * stride) + x1 * pixelSize;
uchar* row = scan0, *bb = row; // <== Z4
for ( int yy = 0; yy < hh; yy++, bb = (row += stride)) {
for (int xx = 0; xx < ww; xx++, bb += pixelSize ) {
// *(bb + 0) is B (Blue ) component of the pixel
// *(bb + 1) is G (Green) component of the pixel
// *(bb + 2) is R (Red ) component of the pixel
// *(bb + 3) is A (Alpha) component of the pixel ( for 32bpp )
uchar gray = ((1140 * *(bb + 0) +
5870 * *(bb + 1) +
2989 * *(bb + 2)) / 10000);
*(bb + 0) = *(bb + 1) = *(bb + 2) = gray;
}
}
...
imwrite(argv[2], bitmap, compression_params); // <== Z5
</uchar>
- 在
Z1
中,我们将图像数据从文件加载到内存。 - 在
Z2
中,我们找到要处理的矩形。 - 在
Z3
中,我们找到位图的scan0
、pixelSize
和stride
。 Z4
中的图像处理代码与我们第一个 C# 程序中的代码相同。- 在
Z5
中,我们将图像写入文件。
基准测试
我运行了每个程序 1000 次,使用 jpg 图像 800x600,并测量每个程序在系统滴答(1 毫秒 = 10000 滴答)中花费的平均时间。
当我在我的计算机(Intel Core i7(6700K) 机器)上运行基准测试时,我得到了以下结果:
正如我们所见:
C#(FastBitmap 和指针)
方法将C#(Bitmap GetPixel 和 SetPixel)
方法的速度提高了 80%。- 似乎
C++(opencv)
方法与C#(FastBitmap 和指针)
方法相比,速度提升微乎其微(不到 2% 的提升)。
历史
- 版本 #4 - 更新到 VS2019,将代码移至 Github
- 版本 #3 - 更新了代码和示例
- 版本 #2 - 添加了基准测试
- 版本 #1 - 重新发布了 C# 中快速图像处理