How do you convert a byte array to a Bitmap instance (.NET)?
Asked Answered
T

4

15

I am working with cameras (in this case monochrome) in VB.NET. The API of the cameras is in C++ and the manufacturer provides wrappers. As is typical in these cases, the image is returned as a Byte array. In my particular case it's 8 bits per pixel so there's no fancy bitpacking to undo.

I was flabbergasted that .NET has so little support for this type of thing. Searching online I found various topics, but it doesn't help that in many cases people have a byte array corresponding to image data (that is, read an image file into a byte array and then have .NET interpret the array as a file, with header and everything).

In this case I need to convert an array of raw pixel values into, particularly, something I can display on-screen.

There doesn't seem to be any built-in way to do this, because the 8bpp format built into .NET is indexed (needs a palette) but it doesn't seem that setting the palette is supported. Again, this really surprised me. This is the most promising topic I found.

So the only way I found to do this is to create a Bitmap and then copy the pixel values pixel by pixel. In my case an 8 MPixel image took several seconds on the fastest i7 when using SetPixel.

The alternative I found to using SetPixel is LockBits, which then gives you access to the (unmanaged) array of bytes that constitutes the image. It seems wasteful, because I have to manipulate the array in .NET and then copy it back to unmananaged space. I'm already copying the original image data once (so the original can be reused or discarded by the rest of what's going on) so although significantly faster than using SetPixel this still seems wasteful.

Here is the VB code I have:

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

For an 8 MPixel image the sequential version consumes about 220 milliseconds from creating TheBitmap up to (and including) the call to TheBitmap.UnlockBits(). The parallel version is much more inconsistent but ranges between 60 and 80 milliseconds. Considering I'm acquiring from four cameras simultaneously and streaming to disk with plenty of time to spare, this is pretty disappointing. The API comes with sample code in C++ which can display from all four cameras simultaneously at full frame rate (17 fps). It never deals with Bitmap, of course, since GDI accesses arrays of raw pixel values.

In summary, I have to travel across the managed/unmanaged barrier simply because there is no direct way to take an array of pixel values and "stuff" it into a Bitmap instance. In this case I'm also copying the pixel values into an 24bpp Bitmap because I cannot "set" the palette in an indexed image so 8bpp is not a viable format for display.

Is there no better way?

Thorp answered 18/4, 2012 at 16:32 Comment(0)
G
11

Create a valid bitmap header, and preappend it to the byte array. You'll only need to manually create 56-128 bytes of data.

Second, create a stream from a byte array.

Next, create an image or bitmap class by using Image.FromStream()/Bitmap.FromStream()

I wouldn't imagine that there is any sort of raw imaging features in .Net. There is so much needed information for an image, it would be an extremely lengthy parameter list for a method to accept raw bytes and create an imagine (is it a bitmap, jpeg, gif, png, etc, and all the additional data each of those requires to create a valid image) it wouldn't make sense.

Guthrey answered 18/4, 2012 at 17:1 Comment(7)
FromStream requires that the stream contain a "full" bitmap (with header info), not just pixel values---I tried to explain this in my question.Thorp
Updated with reference to bmp header info. Pre-appending some 128 bytes then using fromstream should be substanically faster.Guthrey
It's worth a try, especially if it will let me prepend a bitmap header with a greyscale palette so I don't have to copy pixel values 3 times to get 24 bpp.Thorp
This was the answer. Even recreating the header each time and copying the array more than once it's down to 10 milliseconds.Thorp
I should add that the MemoryStream I'm passing has the contents of an 8bpp BMP with a greyscale palette---so by doing this I've avoided having to manually convert to a 24bpp bitmap.Thorp
Another update: I had to put a call to System.GC.Collect() in the function that creates the bitmap. Otherwise, immense amounts of memory would be consumed (upwards of 15 GB) before the garbage collector would run resulting in a long pause (and loss of frames) when it finally did. When the GC runs the time jumps back up to something on the order of 100 ms, but it keeps any long pauses and loss of frames from occurring.Thorp
I wonder if you could reuse your array/bitmap instead of recreating it each time, as that would overwrite the current data in memory and not require any garbage collection at all. I don't know enough about how to do that, just that it better to reuse memory than allocate more for new object while deallocating memory for old objects. Just a thought :)Guthrey
B
7

Try this

var img = new Bitmap(new MemoryStream(yourByteArray));
Banc answered 15/2, 2013 at 15:9 Comment(1)
I think you don't have to declare it as "var". You can declare it as "Bitmap" instead, don't you? But the answer is superb, of course!Lafreniere
A
4

Seems like all your cameras are of same type, so the images data. I don't know if this is possible in your case, but could you create a this "full bitmap" from any image and then just use header as a template (since it will be same for all images).

Abundant answered 18/4, 2012 at 17:13 Comment(0)
I
0

Here is some concise code without the need to write Bitmap header, nor the copying.

    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
Intermarriage answered 19/9, 2023 at 17:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.