在 C# 中快速处理位图

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1563038/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 18:54:01  来源:igfitidea点击:

Fast work with Bitmaps in C#

c#.netgraphicsbitmappixels

提问by AndreyAkinshin

I need to access each pixel of a Bitmap, work with them, then save them to a Bitmap.

我需要访问位图的每个像素,使用它们,然后将它们保存到位图。

Using Bitmap.GetPixel()and Bitmap.SetPixel(), my program runs slowly.

使用Bitmap.GetPixel()and Bitmap.SetPixel(),我的程序运行缓慢。

How can I quickly convert Bitmapto byte[]and back?

我怎么能快速转换Bitmapbyte[]和回?

I need a byte[]with length = (4 * width * height), containing RGBA data of each pixel.

我需要一个byte[]with length = (4 * width * height),包含每个像素的 RGBA 数据。

采纳答案by davidtbernal

You can do it a couple of different ways. You can use unsafeto get direct access to the data, or you can use marshaling to copy the data back and forth. The unsafe code is faster, but marshaling doesn't require unsafe code. Here's a performance comparisonI did a while back.

您可以通过几种不同的方式来做到这一点。您可以使用unsafe直接访问数据,也可以使用封送处理来回复制数据。不安全代码更快,但封送处理不需要不安全代码。这是我不久前进行的性能比较

Here's a complete sample using lockbits:

这是使用锁定位的完整示例:

/*Note unsafe keyword*/
public unsafe Image ThresholdUA(float thresh)
{
    Bitmap b = new Bitmap(_image);//note this has several overloads, including a path to an image

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat);

    /*This time we convert the IntPtr to a ptr*/
    byte* scan0 = (byte*)bData.Scan0.ToPointer();

    for (int i = 0; i < bData.Height; ++i)
    {
        for (int j = 0; j < bData.Width; ++j)
        {
            byte* data = scan0 + i * bData.Stride + j * bitsPerPixel / 8;

            //data is a pointer to the first byte of the 3-byte color data
            //data[0] = blueComponent;
            //data[1] = greenComponent;
            //data[2] = redComponent;
        }
    }

    b.UnlockBits(bData);

    return b;
}

Here's the same thing, but with marshaling:

这是同样的事情,但有编组:

/*No unsafe keyword!*/
public Image ThresholdMA(float thresh)
{
    Bitmap b = new Bitmap(_image);

    BitmapData bData = b.LockBits(new Rectangle(0, 0, _image.Width, _image.Height), ImageLockMode.ReadWrite, b.PixelFormat);

    /* GetBitsPerPixel just does a switch on the PixelFormat and returns the number */
    byte bitsPerPixel = GetBitsPerPixel(bData.PixelFormat);

    /*the size of the image in bytes */
    int size = bData.Stride * bData.Height;

    /*Allocate buffer for image*/
    byte[] data = new byte[size];

    /*This overload copies data of /size/ into /data/ from location specified (/Scan0/)*/
    System.Runtime.InteropServices.Marshal.Copy(bData.Scan0, data, 0, size);

    for (int i = 0; i < size; i += bitsPerPixel / 8 )
    {
        double magnitude = 1/3d*(data[i] +data[i + 1] +data[i + 2]);

        //data[i] is the first of 3 bytes of color

    }

    /* This override copies the data back into the location specified */
    System.Runtime.InteropServices.Marshal.Copy(data, 0, bData.Scan0, data.Length);

    b.UnlockBits(bData);

    return b;
}

回答by David Seiler

You want LockBits. You can then extract the bytes you want from the BitmapData object it gives you.

你想要LockBits。然后,您可以从它提供给您的 BitmapData 对象中提取您想要的字节。

回答by cjbarth

Building on @notJim answer (and with help from http://www.bobpowell.net/lockingbits.htm), I developed the following that makes my life a lot easier in that I end up with an array of arrays that allows me to jump to a pixel by it's xand ycoordinates. Of course, the xcoordinate needs to be corrected for by the number of bytes per pixel, but that is an easy extension.

以@notJim 的回答为基础(并在http://www.bobpowell.net/lockingbits.htm 的帮助下),我开发了以下内容,使我的生活更轻松,因为我最终得到了一组数组,允许我通过它的xy坐标跳转到一个像素。当然,x坐标需要通过每个像素的字节数进行校正,但这是一个简单的扩展。

Dim bitmapData As Imaging.BitmapData = myBitmap.LockBits(New Rectangle(0, 0, myBitmap.Width, myBitmap.Height), Imaging.ImageLockMode.ReadOnly, myBitmap.PixelFormat)

Dim size As Integer = Math.Abs(bitmapData.Stride) * bitmapData.Height
Dim data(size - 1) As Byte

Marshal.Copy(bitmapData.Scan0, data, 0, size)

Dim pixelArray(myBitmap.Height)() As Byte

'we have to load all the opacity pixels into an array for later scanning by column
'the data comes in rows
For y = myBitmap.Height - 1 To 0 Step -1
    Dim rowArray(bitmapData.Stride) As Byte
    Array.Copy(data, y * bitmapData.Stride, rowArray, 0, bitmapData.Stride)
    'For x = myBitmap.Width - 1 To 0 Step -1
    '   Dim i = (y * bitmapData.Stride) + (x * 4)
    '   Dim B = data(i)
    '   Dim G = data(i + 1)
    '   Dim R = data(i + 2)
    '   Dim A = data(i + 3)
    'Next
    pixelArray(y) = rowArray
Next

回答by Daniklad

There is another way that is way faster and much more convenient. If you have a look at the Bitmap constructors you will find one that takes and IntPtr as the last parameter. That IntPtr is for holding pixel data. So how do you use it?

还有另一种方法更快,更方便。如果您查看 Bitmap 构造函数,您会发现一个将 IntPtr 作为最后一个参数的构造函数。该 IntPtr 用于保存像素数据。那么你如何使用它呢?

Dim imageWidth As Integer = 1920
Dim imageHeight As Integer = 1080

Dim fmt As PixelFormat = PixelFormat.Format32bppRgb
Dim pixelFormatSize As Integer = Image.GetPixelFormatSize(fmt)

Dim stride As Integer = imageWidth * pixelFormatSize
Dim padding = 32 - (stride Mod 32)
If padding < 32 Then stride += padding

Dim pixels((stride \ 32) * imageHeight) As Integer
Dim handle As GCHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned)
Dim addr As IntPtr = Marshal.UnsafeAddrOfPinnedArrayElement(pixels, 0)

Dim bitmap As New Bitmap(imageWidth, imageHeight, stride \ 8, fmt, addr)

What you have now is a simple Integer array and a Bitmap referencing the same memory. Any changes you make to the Integer array will be directly affecting the Bitmap. Let us try this with a simple brightness transform.

您现在拥有的是一个简单的整数数组和一个引用相同内存的位图。您对整数数组所做的任何更改都将直接影响位图。让我们用一个简单的亮度变换来试试这个。

Public Sub Brightness(ByRef pixels() As Integer, ByVal scale As Single)
    Dim r, g, b As Integer
    Dim mult As Integer = CInt(1024.0f * scale)
    Dim pixel As Integer

    For i As Integer = 0 To pixels.Length - 1
        pixel = pixels(i)
        r = pixel And 255
        g = (pixel >> 8) And 255
        b = (pixel >> 16) And 255

        'brightness calculation
        'shift right by 10 <=> divide by 1024
        r = (r * mult) >> 10
        g = (g * mult) >> 10
        b = (b * mult) >> 10

        'clamp to between 0 and 255
        If r < 0 Then r = 0
        If g < 0 Then g = 0
        If b < 0 Then b = 0
        r = (r And 255)
        g = (g And 255)
        b = (b And 255)

        pixels(i) = r Or (g << 8) Or (b << 16) Or &HFF000000
    Next
End Sub

You may notice that I have used a little trick to avoid doing floating point math within the loop. This improves performance quite a bit. And when you are done you need to clean up a little of course...

您可能会注意到我使用了一个小技巧来避免在循环中进行浮点运算。这大大提高了性能。当你完成后,你当然需要清理一点......

addr = IntPtr.Zero
If handle.IsAllocated Then
    handle.Free()
    handle = Nothing
End If
bitmap.Dispose()
bitmap = Nothing
pixels = Nothing

I have ignored the alpha component here but you are free to use that as well. I have thrown together a lot of bitmap editing tools this way. It is much faster and more reliable than Bitmap.LockBits() and best of all, it requires zero memory copying to start editing your bitmap.

我在这里忽略了 alpha 组件,但您也可以自由使用它。我用这种方式拼凑了很多位图编辑工具。它比 Bitmap.LockBits() 更快、更可靠,最重要的是,它需要零内存复制才能开始编辑位图。

回答by turgay

You can use Bitmap.LockBits method. Also if you want to use parallel task execution, you can use the Parallel class in System.Threading.Tasks namespace. Following links have some samples and explanations.

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

回答by WonderWorker

Try this C# solution.

试试这个 C# 解决方案。

Create a winforms app for testing.

创建一个用于测试的 winforms 应用程序。

Add a Button and a PictureBox, and a click event and a form closing event.

添加一个按钮和一个图片框,以及一个单击事件和一个窗体关闭事件。

Use the following code for your form:

为您的表单使用以下代码:

public partial class Form1 : Form
{
    uint[] _Pixels { get; set; }

    Bitmap _Bitmap { get; set; }

    GCHandle _Handle { get; set; }

    IntPtr _Addr { get; set; }


    public Form1()
    {
        InitializeComponent();

        int imageWidth = 100; //1920;

        int imageHeight = 100; // 1080;

        PixelFormat fmt = PixelFormat.Format32bppRgb;

        int pixelFormatSize = Image.GetPixelFormatSize(fmt);

        int stride = imageWidth * pixelFormatSize;

        int padding = 32 - (stride % 32);

        if (padding < 32)
        {
            stride += padding;
        }

        _Pixels = new uint[(stride / 32) * imageHeight + 1];

         _Handle = GCHandle.Alloc(_Pixels, GCHandleType.Pinned);

        _Addr = Marshal.UnsafeAddrOfPinnedArrayElement(_Pixels, 0);

        _Bitmap = new Bitmap(imageWidth, imageHeight, stride / 8, fmt, _Addr);

        pictureBox1.Image = _Bitmap;

    }

    private void button1_Click(object sender, EventArgs e)
    {
        for (int i = 0; i < _Pixels.Length; i++)
        {
            _Pixels[i] = ((uint)(255 | (255 << 8) | (255 << 16) | 0xff000000));

        }

    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        _Addr = IntPtr.Zero;

        if (_Handle.IsAllocated)
        {
            _Handle.Free();

        }

        _Bitmap.Dispose();

        _Bitmap = null;

        _Pixels = null;

    }

}

Now, any edits you make to the array will automatically update the Bitmap.

现在,您对数组所做的任何编辑都会自动更新位图。

You will need to call the refresh method on the picturebox to see these changes.

您需要调用图片框上的 refresh 方法才能查看这些更改。

回答by Shaamil Ahmed

If you're on C# 8.0 I'll suggest to use the new Span<T>for higher efficiency.

如果您使用的是 C# 8.0,我建议您使用新的Span<T>以提高效率。

Here's a rough implementation

这是一个粗略的实现

public unsafe class FastBitmap : IDisposable
{
    private Bitmap _bmp;
    private ImageLockMode _lockmode;
    private int _pixelLength;

    private Rectangle _rect;
    private BitmapData _data;
    private byte* _bufferPtr;

    public int Width { get => _bmp.Width; }
    public int Height { get => _bmp.Height; }
    public PixelFormat PixelFormat { get => _bmp.PixelFormat; }

    public FastBitmap(Bitmap bmp, ImageLockMode lockMode)
    {
        _bmp = bmp;
        _lockmode = lockMode;

        _pixelLength = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
        _rect = new Rectangle(0, 0, Width, Height);
        _data = bmp.LockBits(_rect, lockMode, PixelFormat);
        _bufferPtr = (byte*)_data.Scan0.ToPointer();
    }

    public Span<byte> this[int x, int y]
    {
        get
        {
            var pixel = _bufferPtr + y * _data.Stride * x * _pixelLength;
            return new Span<byte>(pixel, _pixelLength);
        }
        set
        {
            value.CopyTo(this[x, y]);
        }
    }

    public void Dispose()
    {
        _bmp.UnlockBits(_data);
    }
}