如何将一个字节数组转换为位图实例(.NET)?

15

我正在使用VB.NET处理相机(在这种情况下是单色相机)。 相机的API是用C++编写的,并且制造商提供了包装器。 在这些情况下往往返回图像作为 Byte 数组。在我的特定情况下,每个像素有8位,因此没有需要解开的复杂的位压缩。

我感到非常震惊的是 .NET 对于这种类型的事情提供的支持如此之少。 在网上搜索后,我发现各种主题,但是问题在于在许多情况下人们都有一个字节数组对应于图像数据(也就是将图像文件读入字节数组,然后使 .NET 将该数组解释为带标题等内容的文件)。

在这种情况下,我需要将一系列原始像素值转换为可以在屏幕上显示的东西。

似乎没有内置的方法来完成此操作,因为.NET中内置的8bpp格式是索引格式(需要调色板),但似乎不支持设置调色板。再次惊讶。 是我找到的最有前途的主题。

因此,我找到的唯一方法是创建一个 Bitmap,然后逐像素复制像素值。在我这种情况下,使用SetPixel 在最快的i7上需要几秒钟来处理一个800万像素的图像。

我发现使用 LockBits 是替代使用 SetPixel 的一种方法,它可以让你访问(不受管理的)字节数组,它构成了图像。这似乎是浪费的,因为我必须在.NET中操纵数组,然后将其复制回不受管理的空间。我已经复制了原始图像数据一次(这样原始数据可以被重用或丢弃),因此尽管比使用SetPixel 快得多,但仍然看起来有些浪费。

下面是我所拥有的VB代码:

Public Function GetBitmap(ByRef TheBitmap As System.Drawing.Bitmap) As Integer
    Dim ImageData() As Byte
    Dim TheWidth, TheHeight As Integer
    'I've omitted the code here (too specific for this question) which allocates
    'ImageData and then uses Array.Copy to store a copy of the pixel values
    'in it. It also assigns the proper values to TheWidth and TheHeight.

    TheBitmap = New System.Drawing.Bitmap(TheWidth, TheHeight, System.Drawing.Imaging.PixelFormat.Format24bppRgb)
    Dim TheData As System.Drawing.Imaging.BitmapData = TheBitmap.LockBits(
        New System.Drawing.Rectangle(0, 0, TheWidth, TheHeight), Drawing.Imaging.ImageLockMode.ReadWrite, TheBitmap.PixelFormat)
    Dim Image24(Math.Abs(TheData.Stride) * TheHeight) As Byte
    'Then we have two choices: to do it in parallel or not. I tried both; the sequential version is commented out below.
    System.Threading.Tasks.Parallel.For(0, ImageData.Length, Sub(i)
                                                                 Image24(i * 3 + 0) = ImageData(i)
                                                                 Image24(i * 3 + 1) = ImageData(i)
                                                                 Image24(i * 3 + 2) = ImageData(i)
                                                             End Sub)
    'Dim i As Integer
    'For i = 0 To ImageData.Length - 1
    '    Image24(i * 3 + 0) = ImageData(i)
    '    Image24(i * 3 + 1) = ImageData(i)
    '    Image24(i * 3 + 2) = ImageData(i)
    'Next
    System.Runtime.InteropServices.Marshal.Copy(Image24, 0, TheData.Scan0, Image24.Length)
    TheBitmap.UnlockBits(TheData)

    'The above based on
    'http://msdn.microsoft.com/en-us/library/system.drawing.imaging.bitmapdata.aspx

    Return 1

End Function
对于一个8兆像素的图像,串行版本从创建TheBitmap到调用TheBitmap.UnlockBits()大约需要消耗220毫秒。并行版本不太一致,但范围在60到80毫秒之间。考虑到我同时从四个相机获取数据,并将数据流式传输到磁盘上,而且时间还很充裕,这令人非常失望。该API附带有C ++示例代码,可以以全帧率(17 fps)同时显示来自四个相机的内容。当然,它从未涉及到Bitmap,因为GDI访问原始像素值的数组。

总之,我必须穿越托管/非托管的边界,只是因为没有直接的方法可以将像素值数组插入到Bitmap实例中。在这种情况下,我还将像素值复制到24bpp Bitmap中,因为我无法在索引图像中“设置”调色板,所以8bpp不是可用于显示的格式。

难道就没有更好的方法吗?
4个回答

11
创建一个有效的位图头,并将其预先附加到字节数组中。您只需要手动创建56-128字节的数据。
其次,从字节数组中创建流
接下来,使用Image.FromStream()/Bitmap.FromStream()创建图像或位图类 我不认为.NET中有任何原始成像功能。对于一个方法来接受原始字节并创建一个图像(它是位图、jpeg、gif、png等等,以及所有这些要求创建有效图像的额外数据),需要大量所需信息,这将是一个极其冗长的参数列表,这是没有意义的。

3
参考bmp文件头信息后更新。在使用fromstream之前,预先添加128字节可以显著提高速度。 - Erik Philips
1
值得一试,特别是如果它能让我在位图头部添加灰度调色板,这样我就不必复制像素值三次以获得24 bpp。 - darda
1
这就是答案。即使每次重新创建标题并多次复制数组,时间已经缩短到10毫秒。 - darda
1
另一个更新:我不得不在创建位图的函数中调用System.GC.Collect()。否则,将会消耗大量内存(高达15 GB),在垃圾回收器运行之前会导致长时间暂停(以及丢失帧),最终垃圾回收器运行时时间会跳回到大约100毫秒左右,但它可以避免发生任何长时间暂停和丢失帧。 - darda
1
我想知道你是否可以重复使用你的数组/位图,而不是每次重新创建它,因为这样会覆盖内存中的当前数据,并且不需要任何垃圾回收。 我不太了解如何做到这一点,只是知道重复使用内存比为新对象分配更多内存并释放旧对象的内存更好。 只是一个想法 :) - Erik Philips
显示剩余2条评论

7

试试这个

var img = new Bitmap(new MemoryStream(yourByteArray));

1
我认为你不需要将它声明为"var",而可以声明为"Bitmap",是吗?当然,答案非常棒! - tmighty

4

看起来你所有的相机都是同一类型,所以图像数据也是如此。我不知道在你的情况下是否可能,但你能否从任何一个图像中创建这个“完整位图”,然后只使用标题作为模板(因为它对于所有图像都是相同的)。


0
这里有一些简洁的代码,无需编写位图头,也无需复制。
    Dim XyBmp As Bitmap
    Dim ColorPalette As Imaging.ColorPalette
    Dim XyBuffer(0 To 128 * 256 - 1) As Byte
    Dim XyBufferHandle As GCHandle

    
    Private Sub createXyBmp()
        Dim i As Integer

        XyBufferHandle = GCHandle.Alloc(XyBuffer, GCHandleType.Pinned)

        XyBmp = New Bitmap(128, 256, 128, Imaging.PixelFormat.Format8bppIndexed, XyBufferHandle.AddrOfPinnedObject())
        ColorPalette = XyBmp.Palette
        For i = 0 To 255
            ColorPalette.Entries(i) = Color.FromArgb(255, i, i, i)
        Next i
        XyBmp.Palette = ColorPalette
    End Sub

    Friend Sub DrawXyBmp()
        Do
            Static b As Byte
            For i = 0 To XyBuffer.Length - 1
                XyBuffer(i) = b
            Next i

            Me.Refresh()
            Me.BackgroundImage = XyBmp
            b = CByte((b + 1) And &HFF)
        Loop
    End Sub

Private Sub close()
    XyBmp.Dispose()
    XyBufferHandle.Free()
End Sub

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