65.9K
CodeProject 正在变化。 阅读更多。
Home

在图像处理中使用指针

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (9投票s)

2019年6月17日

CPOL

6分钟阅读

viewsIcon

18561

downloadIcon

496

如何在 C# 中处理图像时使用指针

引言

使用 Bitmap 类及其方法 GetPixelSetPixel 来访问位图像素速度很慢。在本文中,我们将看到如何通过 FastBitmap 类来提高访问位图像素的速度。

正如 C# 中快速图像处理 的一些读者所建议的,我们还将编写 3 个类似的程序并比较它们的性能。这些程序将图像的中心替换为其灰度版本。

  • 程序 #1 将使用 FastBitmap
  • 程序 #2 将使用 Bitmap 类及其方法 GetPixelSetPixel()
  • 程序 #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.Stridefb.PixelSize * 位图宽度,所以 fb.Stride=9*3=27
  • fb.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 类的 SetPixelGetPixel 方法,而不是指针。

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 中,我们找到位图的 scan0pixelSizestride
  • 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# 中快速图像处理
© . All rights reserved.