【问题标题】:C# - Faster Alternatives to SetPixel and GetPixel for Bitmaps for Windows Forms AppC# - 用于 Windows 窗体应用程序的位图的 SetPixel 和 GetPixel 的更快替代方案
【发布时间】:2014-09-02 07:31:33
【问题描述】:

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

【问题讨论】:

  • 此处的所有答案仅支持特定的像素格式。如果需要快速和简单的解决方案,您可以使用this library(免责声明:由我撰写)。

标签: c# drawing gdi+ getpixel


【解决方案1】:

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

  BitmapData BtmpDt = a.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);

其中 btm 是位图变量。

【讨论】:

    【解决方案2】:

    立即可用的代码

    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 on Wikipedia 了解有关其工作原理的更多信息,并参阅examples on the MSDN article for 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 函数将需要相应地重新设计或删除。

    使用上述类的好处

    • 仅用于操作数据的内存分配是不必要的;对原始数据所做的更改会立即应用于位图。
    • 没有其他需要管理的对象。这实现了IDisposable,就像Bitmap
    • 它不需要unsafe 块。

    注意事项

    • 无法移动固定内存。为了使这种内存访问起作用,这是必需的副作用。这会降低垃圾收集器 (MSDN Article) 的效率。仅对需要性能的位图执行此操作,并确保在完成后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 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.
    

    【讨论】:

    • 它可能没有作为默认提供,因为它是一个非托管对象(相反,底层数据是非托管的)并且与框架的理念背道而驰。但是这个版本对于频繁的图像处理肯定更有用。
    • DirectBitmap 需要从头开始创建。如果您需要从现有的 Bitmap 创建一个,则需要创建一个具有相同尺寸的 DirectBitmap,并使用 Graphics 对象将其复制过来。
    • @SaxxonPike 你能解释一下你是如何从 Graphics 复制到 DirectBitmap 的吗?如果可能的话,DirectBitmap 的用例示例会很棒。
    • @SaxxonPike 抱歉问,但我将如何使用这个类来做一些事情,比如'img.SetPixel(x,y,Color.Transparent);' ?我知道如何创建类并使构造函数适应我的需要,但我似乎不知道如何使用这些位来改变像素的颜色。再次抱歉,我从来没有研究过比特或字节。
    • 我建议修改此代码以在~DirectBitmap() finalize 方法中调用Dispose(),或者提供在using (DirectBitmap bmp = new DirectBitmap()) { ... } 块中创建DirectBitmap 的示例用法。
    【解决方案3】:

    您可以使用 Bitmap.LockBits 方法。此外,如果您想使用并行任务执行,可以使用 System.Threading.Tasks 命名空间中的 Parallel 类。以下链接有一些示例和说明。

    【讨论】:

      【解决方案4】:

      位图操作在 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)
        {
      
        }
      }
      

      【讨论】:

      • 我的猜测是,将整个位图正常保存在内存中效率不高。 1024*1024*4 = 4,194,304 字节 = 4 兆字节。
      • 对不起,没想到按回车时会发送。这些位始终在内存中。问题在于函数调用和查找所需像素的像素格式和位置的开销。使用基于 LockBits 的循环,您只需执行一次,而不是每个像素执行一次。性能提升取决于您的用例(包括图像大小),但请注意 GDI+ 通常表现不佳,不适合实时应用程序。
      • 这里的答案是错误的。为什么要锁定?因为 .NET 使用垃圾收集器异步释放未使用的内存。释放内存块后,它将剩余内存移动到其他位置以获得更长一致的可用内存块。如果垃圾收集器在您读取像素时将您的位图移动到另一个位置,那么您将读取无意义的值。所以.NET 强制你锁定禁止垃圾收集器移动它的位图。位图数据会一直保留在内存中的同一位置,直到您解锁为止。
      • 顺便说一句,可以直接从BitmapData 对象中获取两个跨步。此代码无法深入了解步幅的来源。就此而言,srcTileOffset 是什么也不清楚。
      • tilePart.WidthtilePart.Weight 非常慢。考虑将它们的结果放在单独的宽度/高度变量中。就我而言,这将 2048x2048 图像的性能提高了 40 倍。
      猜你喜欢
      • 1970-01-01
      • 2011-05-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多