F# has fantastic array manipulation functionality.  To leverage this functionality for some very elegant image processing, it is first necessary to to convert image files into a byte arrays.  Unfortunately, this process is not as simple as one might hope. 

It’s a dark path of missing documentation, incorrect code samples and some ugly .NET interop.  With all of the other difficulties involved, I want to otherwise keep things simple and so will make the following assumptions:

  1. The image file will be properly handled by .NET’s image codecs (it’s known to have some issues with Tiffs in particular)
  2. The Image format is 24 bits per pixel BGR
  3. The end user will handle exceptions

By far the most difficult things in writing this small sample was that it seemed every BitmapData implementation I ran into was completely broken.  In fact, this was the case for even the .NET Framework SDK sample code.  For each example I tried the following two tests succeeded:

[<Fact>]

member x.first_matches_GetPixel() =

  let pixel = bmp24Bgr.GetPixel(0,0)

  Assert.Equal( pixel.B, array24Bgr.[0] )

  Assert.Equal( pixel.G, array24Bgr.[1] )

  Assert.Equal( pixel.R, array24Bgr.[2] )

 

[<Fact>]

member x.last_on_first_scanline_matches_GetPixel() =

  let pixel = bmp24Bgr.GetPixel( bmp24Bgr.Width - 1 , 0 )

  let offset = (bmp24Bgr.Width - 1) * 3

  Assert.Equal( pixel.B, array24Bgr.[offset] )

  Assert.Equal( pixel.G, array24Bgr.[offset + 1] )

  Assert.Equal( pixel.R, array24Bgr.[offset + 2] )

Unfortunately, the following two tests would fail:

[<Fact>]

member x.first_on_last_scanline_matches_GetPixel() =

  let pixel = bmp24Bgr.GetPixel( 0 , bmp24Bgr.Height - 1 )

  let offset = bmp24Bgr.Width * (bmp24Bgr.Height - 1) * 3

  Assert.Equal( pixel.B, array24Bgr.[offset] )

  Assert.Equal( pixel.G, array24Bgr.[offset + 1] )

  Assert.Equal( pixel.R, array24Bgr.[offset + 2] )

 

[<Fact>]

member x.last_matches_GetPixel() =

  let pixel = bmp24Bgr.GetPixel( bmp24Bgr.Width - 1,

                                 bmp24Bgr.Height - 1 )

  let offset = (bmp24Bgr.Width * bmp24Bgr.Height * 3) - 3

  Assert.Equal( pixel.B, array24Bgr.[offset ] )

  Assert.Equal( pixel.G, array24Bgr.[offset + 1] )

  Assert.Equal( pixel.R, array24Bgr.[offset + 2] )

On closer inspection it seemed something was turning everything after the first scanline into garbage.

Well, it turns out that each scanline in BitmapData’s allocated memory block can have some padding on the end.  This mean’s you can’t simply block-copy the memory into your array as is done in so many examples.  Instead, it is necessary to iterate over each scanline and copy only up to the padding.  A fact missing from everywhere but a small corner of the .NET Framework BitmapData documentation.

A big thanks to Bob Powell for his article on exactly how the BitmapData class works.  Once I saw his diagram, I knew exactly what was going on and reworked my code to Marshal the data out of each scanline separately, minus the padding.

open System.Drawing

open System.Drawing.Imaging

open System.Runtime.InteropServices

 

let getBytesFromBitmap (bytesPerPixel: int) (bmp: Bitmap) =

  let imgRect = new Rectangle(0, 0, bmp.Width, bmp.Height)

  let bmpBits = bmp.LockBits(imgRect, ImageLockMode.ReadOnly,

                             bmp.PixelFormat)

  try

    let pixelBytes = bmp.Height * bmp.Width * bytesPerPixel

    let byteArray: byte[] = Array.zeroCreate pixelBytes

 

    let scanlineBytes = bmp.Width * bytesPerPixel

    let stride: nativeint = nativeint bmpBits.Stride

    for i in [ 0 .. bmp.Height - 1 ] do

        let bmpOffset = stride * nativeint i

        let arrayOffset = scanlineBytes * int i

        Marshal.Copy( bmpBits.Scan0 + bmpOffset, byteArray,

                      arrayOffset, scanlineBytes)

    byteArray

  finally       

    bmp.UnlockBits( bmpBits )

This code is not as pretty or efficient as it might be, but at least it works and is fairly safe.  I’d love to clean this up and so I encourage you to leave critiques and/or example code in the comments section.

Also, I’ve made my Visual Studio 2008 solution containing the above code and a few small extras available.  Let me say again that any comments or critiques are welcome.  I hope that in posting this I’ve saved you the some of the pain I had in writing it.

Ninja Edit:  The idea of this function exiting with still locked bits was really bothering me and so I put everything after LockBits into a try-finally.