【问题标题】:Fast work with Bitmaps in C#在 C# 中快速使用位图
【发布时间】:2010-12-06 11:45:02
【问题描述】:

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

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

如何快速将Bitmap 转换为byte[] 并返回?

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

【问题讨论】:

  • 以下链接比较包含位图像素访问方法。 csharpexamples.com/fast-image-processing-c
  • 这里的答案推荐使用LockBits,它需要对所有可能的像素格式进行不同的访问。 Here 是一个库(免责声明:由我编写),它为所有可能的格式(包括索引格式)提供快速、统一的托管访问。

标签: c# .net graphics bitmap pixels


【解决方案1】:

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

【讨论】:

  • 我认为,如果您在带有指针转换的 unsafe 块内使用从 LockBits 返回的 BitmapData 对象会更快(注意:我实际上不知道这是否更快,但我'正在尝试刺激其他人对其进行基准测试)。
  • 它会为您保存进出 BitmapData 的副本,这很好。不过,不确定在实践中会节省多少;这些天 memcpy() 真的很快。
【解决方案2】:

在@notJim 回答的基础上(在https://web.archive.org/web/20141229164101/http://bobpowell.net/lockingbits.aspx 的帮助下),我开发了以下内容,让我的生活变得更轻松,因为我最终得到了一个数组数组,允许我通过@ 跳转到一个像素987654322@ 和 y 坐标。当然,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

【讨论】:

    【解决方案3】:

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

    这是一个粗略的实现

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

    【讨论】:

    • 如果我想写入位图,我将如何使用它?
    • 我已经更新了代码并且应该像一个魅力一样工作。现在您可以通过说new FastBitmap(.....)[100, 200] 访问像素,其中 100 是 x,200 是 y。可以通过写new FastBitmap(......)[100, 200] = new Span&lt;byte&gt;(array);来设置像素的值
    • 如何将其转换回位图?我可以公开 _data 吗?
    • gyazo.com/d5ab06cf35a3bb84a56d8330fa25517b 我自己做了演员,但在尝试fastbmp[x,y] = new Span&lt;byte&gt;(BitConverter.GetBytes(color.ToArgb()));时遇到了这个错误
    • 不是var pixel = _bufferPtr + y * _data.Stride * x * _pixelLength; 应该是var pixel = _bufferPtr + y * _data.Stride + x * _pixelLength
    【解决方案4】:

    您可以通过几种不同的方式进行操作。您可以使用unsafe 直接访问数据,也可以使用封送处理来回复制数据。不安全代码更快,但编组不需要不安全代码。这是我不久前做过的performance comparison

    这是使用 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;
    }
    

    这里是同样的东西,但有编组:

    /*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;
    }
    

    【讨论】:

    • 谢谢,我什至不必激怒任何人。 :)
    • 您在执行此操作时应该注意,颜色通道的顺序可能会根据您使用的 PixelFormat 有所不同。例如,对于 24 位位图,像素的第一个字节是蓝色通道,然后是绿色,然后是红色,这与通常预期的红-绿-蓝顺序相反。
    • @Parhs 当然,跳过bData.Height 会加快速度,因为您只获取第一行数据。试试size = 1,这会让事情变得更快!
    • 您还应该知道,这可能不会得到预期的字节数据,因为通常bData.Stride 不必等于b.Width * bytePerPix
    • 这里的GetBitsPerPixel() 是什么?
    【解决方案5】:

    试试这个 C# 解决方案。

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

    添加一个Button和一个PictureBox,以及一个点击事件和一个表单关闭事件。

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

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

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

    您需要调用图片框上的刷新方法才能看到这些变化。

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        还有另一种更快、更方便的方法。如果您查看 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)
        

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

        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
        

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-01-27
          • 2010-12-09
          • 1970-01-01
          • 2014-12-10
          • 1970-01-01
          相关资源
          最近更新 更多