C# - 用于Windows Forms应用程序的比SetPixel和GetPixel更快的位图处理方法

63

我正在尝试自学C#,听说get和setpixel函数非常慢。有什么替代方法,性能提升真的那么显著吗?

以下是我的一段代码:

public static Bitmap Paint(Bitmap _b, Color f)
{
  Bitmap b = new Bitmap(_b);
  for (int x = 0; x < b.Width; x++) 
  {
    for (int y = 0; y < b.Height; y++) 
    {
      Color c = b.GetPixel(x, y);
      b.SetPixel(x, y, Color.FromArgb(c.A, f.R, f.G, f.B));
    }
  }
  return b;
}

1
这里的所有答案都只支持特定像素格式。如果需要快速简单的解决方案,您可以使用此库(免责声明:由我编写)。 - György Kőszeg
5个回答

137

可立即使用的代码

public class DirectBitmap : IDisposable
{
    public Bitmap Bitmap { get; private set; }
    public Int32[] Bits { get; private set; }
    public bool Disposed { get; private set; }
    public int Height { get; private set; }
    public int Width { get; private set; }

    protected GCHandle BitsHandle { get; private set; }

    public DirectBitmap(int width, int height)
    {
        Width = width;
        Height = height;
        Bits = new Int32[width * height];
        BitsHandle = GCHandle.Alloc(Bits, GCHandleType.Pinned);
        Bitmap = new Bitmap(width, height, width * 4, PixelFormat.Format32bppPArgb, BitsHandle.AddrOfPinnedObject());
    }

    public void SetPixel(int x, int y, Color colour)
    {
        int index = x + (y * Width);
        int col = colour.ToArgb();

        Bits[index] = col;
    }

    public Color GetPixel(int x, int y)
    {
        int index = x + (y * Width);
        int col = Bits[index];
        Color result = Color.FromArgb(col);

        return result;
    }

    public void Dispose()
    {
        if (Disposed) return;
        Disposed = true;
        Bitmap.Dispose();
        BitsHandle.Free();
    }
}

不需要使用LockBitsSetPixel。使用上述类来直接访问位图数据。

使用此类,可以将原始位图数据设置为32位数据。请注意,它是PARGB,即预乘 alpha。有关如何工作的更多信息,请参见维基百科上的 Alpha Compositing,以及MSDN文章中有关 BLENDFUNCTION 的示例以了解如何正确计算 alpha。

如果预乘可能会使事情过于复杂,请改用PixelFormat.Format32bppArgb。当其被绘制时会出现性能损失,因为它在内部被转换为PixelFormat.Format32bppPArgb。如果在绘制之前图像不必更改,则可以在预乘之前完成工作,将其绘制到PixelFormat.Format32bppArgb缓冲区中,并从那里进一步使用。

通过Bitmap属性可以访问标准的Bitmap成员。使用Bits属性可以直接访问位图数据。

使用byte而不是int来处理原始像素数据

将两个实例中的Int32都改为byte,然后更改此行:

Bits = new Int32[width * height];

变成这样:

Bits = new byte[width * height * 4];

使用字节时,格式为Alpha / Red / Green / Blue。每个像素需要4个字节的数据,分别为每个通道一个字节。 GetPixel和SetPixel函数需要相应地重新设计或删除。

使用上述类的好处

  • 仅操纵数据而不需要内存分配是不必要的。对原始数据进行的更改立即应用于位图。
  • 没有其他需要管理的对象。这像Bitmap一样实现了IDisposable
  • 它不需要一个 unsafe 块。

需要考虑的问题

  • 固定内存无法移动。这是这种内存访问工作的必需副作用。这会降低垃圾收集器的效率(MSDN文章)。只有在需要性能的位图中才执行此操作,并确保在完成后释放它们以使内存取消固定。

通过Graphics对象访问

因为Bitmap属性实际上是.NETBitmap对象,所以可以使用Graphics类执行操作。

var dbm = new DirectBitmap(200, 200);
using (var g = Graphics.FromImage(dbm.Bitmap))
{
    g.DrawRectangle(Pens.Black, new Rectangle(50, 50, 100, 100));
}

性能比较

这个问题涉及到性能,以下是一张表格,展示了三种不同方法在相对性能方面的比较。该测试使用基于 .NET Standard 2 和 NUnit 的应用程序进行。

* Time to fill the entire bitmap with red pixels *
- Not including the time to create and dispose the bitmap
- Best out of 100 runs taken
- Lower is better
- Time is measured in Stopwatch ticks to emphasize magnitude rather than actual time elapsed
- Tests were performed on an Intel Core i7-4790 based workstation

              Bitmap size
Method        4x4   16x16   64x64   256x256   1024x1024   4096x4096
DirectBitmap  <1    2       28      668       8219        178639
LockBits      2     3       33      670       9612        197115
SetPixel      45    371     5920    97477     1563171     25811013

* Test details *

- LockBits test: Bitmap.LockBits is only called once and the benchmark
                 includes Bitmap.UnlockBits. It is expected that this
                 is the absolute best case, adding more lock/unlock calls
                 will increase the time required to complete the operation.

5
这个功能可能不会默认提供,因为它是一个未经管理的对象(也就是底层数据是未经管理的),这与框架的理念相矛盾。但对于频繁的图像操作来说,这个版本显然更有用。 - marknuzz
4
需要从头开始创建DirectBitmap。如果您需要从现有的Bitmap创建一个DirectBitmap,则需要创建一个具有相同尺寸的DirectBitmap,并使用Graphics对象将其复制过去。 - A.Konzel
1
@SaxxonPike,你能解释一下如何从Graphics复制到DirectBitmap吗?如果可能的话,提供一个DirectBitmap的用例示例会很棒。 - B.K.
2
我建议修改此代码,在~DirectBitmap()终结方法中调用Dispose(),或提供一个示例用法,在using(DirectBitmap bmp = new DirectBitmap()){...}块中创建DirectBitmap。 - Gladclef
1
@Vincent 在这种情况下,我所做的是使用Bitmap.FromStream()从磁盘加载位图,然后创建一个与其大小相同的DirectBitmap,并使用Graphics.DrawImage()从Bitmap到DirectBitmap。现在,您可以直接访问现有位图的像素。 - A.Konzel
显示剩余16条评论

20

在C#中,位图操作速度缓慢的原因在于锁定和解锁。每次操作都需要对所需位进行锁定,操作这些位,然后再解锁这些位。

通过自己处理这些操作,可以大大提高速度。请参考以下示例。

using (var tile = new Bitmap(tilePart.Width, tilePart.Height))
{
  try
  {
      BitmapData srcData = sourceImage.LockBits(tilePart, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
      BitmapData dstData = tile.LockBits(new Rectangle(0, 0, tile.Width, tile.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

      unsafe
      {
          byte* dstPointer = (byte*)dstData.Scan0;
          byte* srcPointer = (byte*)srcData.Scan0;

          for (int i = 0; i < tilePart.Height; i++)
          {
              for (int j = 0; j < tilePart.Width; j++)
              {
                  dstPointer[0] = srcPointer[0]; // Blue
                  dstPointer[1] = srcPointer[1]; // Green
                  dstPointer[2] = srcPointer[2]; // Red
                  dstPointer[3] = srcPointer[3]; // Alpha

                  srcPointer += BytesPerPixel;
                  dstPointer += BytesPerPixel;
              }
              srcPointer += srcStrideOffset + srcTileOffset;
              dstPointer += dstStrideOffset;
          }
      }

      tile.UnlockBits(dstData);
      aSourceImage.UnlockBits(srcData);

      tile.Save(path);
  }
  catch (InvalidOperationException e)
  {

  }
}

1
我的猜测是通常情况下将整个位图保存在内存中不太高效。102410244 = 4,194,304字节= 4兆字节。 - Bort
1
抱歉,没想到按下回车键就发送了。位始终在内存中。问题在于函数调用和查找所需像素的像素格式和位置的开销。基于LockBits的循环只需要执行一次,而不是每个像素都执行一次。性能提升取决于您的用例(包括图像大小),但请注意,GDI+总体表现不佳,不适用于实时应用程序。 - Esme Povirk
10
这里的答案是错误的。为什么要锁定?因为.NET使用垃圾回收器异步释放未使用的内存。在释放一个内存块后,它会将剩余的内存移动到其他位置以获得更长时间一致的空闲内存块。如果垃圾回收器在您读取像素的同时将位图移动到另一个位置,那么您将读取到无意义的值。因此,.NET强制您锁定位图,禁止垃圾回收器对其进行移动。位图数据保持在内存中的相同位置,直到解锁为止。 - Elmue
3
顺便提一下,这两个步幅可以直接从“BitmapData”对象中获取。但是这段代码并没有说明步幅的来源。而且,“srcTileOffset”的含义也不清楚。 - Nyerguds
5
tilePart.WidthtilePart.Height 很慢。考虑将它们的结果放入单独的宽度/高度变量中。在我这个情况下,这样做可以将性能提升40倍,对于2048x2048像素的图像也是如此。 - blade
显示剩余4条评论

8

已经过了一段时间,但我找到了一个可能有用的例子。

var btm = new Bitmap("image.png");

BitmapData btmDt = btm.LockBits(
    new Rectangle(0, 0, btm.Width, btm.Height),
    ImageLockMode.ReadWrite,
    btm.PixelFormat
);
IntPtr pointer = btmDt.Scan0;
int size = Math.Abs(btmDt.Stride) * btm.Height;
byte[] pixels = new byte[size];
Marshal.Copy(pointer, pixels, 0, size);
for (int b = 0; b < pixels.Length; b++)
{
    pixels[b] = 255; //Do something here 
}

Marshal.Copy(pixels, 0, pointer, size);
btm.UnlockBits(btmDt);

1

这段代码应该并行化,同步运行会导致巨大的性能损失。几乎所有现代微芯片都至少有4个可用线程,而一些芯片则有40个可用线程。

没有任何理由让第一个循环同步运行。你可以使用许多线程遍历宽度或长度。

        private void TakeApart_Fast(Bitmap processedBitmap)
        {
            
            BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            ConcurrentBag<byte> points = new ConcurrentBag<byte>();   
            unsafe
            {
                int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
                int heightInPixels = bitmapData.Height;
                int widthInBytes = bitmapData.Width * bytesPerPixel;
                _RedMin = byte.MaxValue;
                _RedMax = byte.MinValue;
                byte* PtrFirstPixel = (byte*)bitmapData.Scan0;
              
                Parallel.For(0, heightInPixels, y =>
                {
                    byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
                    for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                    {
                        // red
                        byte redPixel = currentLine[x + 2];
                        //save information with the concurrentbag
    
                    }
                });
                processedBitmap.UnlockBits(bitmapData);
            }
        }`
  

基准测试并不意味着太多,因为这将加速处理的程度取决于您使用的硬件以及后台运行的其他内容,这完全取决于有多少可用的空闲线程。如果您在4000系列图形卡上运行此程序,并且拥有数千个流处理器,则可以同时迭代图像的每一列。

如果您在旧的四核处理器上运行它,则可能只有5或6个线程,但仍然非常显著。


1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接