c# -为Windows窗体应用程序的位图提供SetPixel和GetPixel的更快替代品

本文关键字:SetPixel GetPixel 替代品 位图 Windows 窗体 应用程序 | 更新日期: 2023-09-27 18:12:43

我正在尝试自学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;
}

c# -为Windows窗体应用程序的位图提供SetPixel和GetPixel的更快替代品

立即可用的代码

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合成,了解更多有关其工作原理的信息,以及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文章)。只有在需要性能的地方才使用位图,并确保在完成后将它们Dispose,以便内存可以解除固定。

通过Graphics对象访问

因为Bitmap属性实际上是一个。net Bitmap对象,所以使用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标准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.

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)
  {
  }
}

已经有一段时间了,但是我发现了一个可能有用的例子。

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);

您可以使用位图。LockBits方法。另外,如果你想使用并行任务执行,你可以使用System.Threading.Tasks命名空间中的parallel类。以下链接有一些示例和解释。

  • http://csharpexamples.com/fast-image-processing-c/
  • http://msdn.microsoft.com/en-us/library/dd460713%28v=vs.110%29.aspx
  • http://msdn.microsoft.com/tr-tr/library/system.drawing.imaging.bitmapdata%28v=vs.110%29.aspx

这段代码应该并行化,同步运行会错过大量的性能增益。几乎没有一个现代微芯片的可用线程少于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个线程,这仍然是非常重要的。