High Performance Image Fun with F# May, 2010
I'm working on a project with a buddy of mine to analyze old reels of scientific data. We're scanning them in and then doing an analysis. I'm not a real expert in the imaging domain so I thought I'd first give the managed GDI+ API a try using the Bitmap object and GetPixel/SetPixel. Needless to say the performance was less than stellar. After a quick search I stumbled upon a page by Bob Powell describing the LockBits and UnlockBits methods on the Bitmap object. Using this method improved performance by several orders of magnitude. I was able to analyze an 80mb tiff in about 15 seconds. Below is an implementation in F#; you can read about the details of using LockBits/UnlockBits on Bob's page.
The following is the LockContext object. This is a disposable type that unlocks when disposed:
open System open System.Drawing open System.Drawing.Imaging open Microsoft.FSharp.NativeInterop type LockContext(bitmap:Bitmap) = let data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat) let formatNotSupportedMessage = "Pixel format not supported." let getPixelAddress = match data.PixelFormat with | PixelFormat.Format24bppRgb -> (fun x y -> NativePtr.add<byte> (NativePtr.ofNativeInt data.Scan0) ((y * data.Stride) + (x * 3))) | _ -> failwith formatNotSupportedMessage let getPixel x y = let address = getPixelAddress x y match data.PixelFormat with | PixelFormat.Format24bppRgb -> Color.FromArgb(NativePtr.get address 2 |> int, NativePtr.get address 1 |> int, NativePtr.read address |> int) | _ -> failwith formatNotSupportedMessage let setPixel x y (r,g,b) = let address = getPixelAddress x y match data.PixelFormat with | PixelFormat.Format24bppRgb -> NativePtr.set address 2 r NativePtr.set address 1 g NativePtr.write address b | _ -> failwith formatNotSupportedMessage member this.SetPixel(x,y,color:Color) = setPixel x y (color.R, color.G, color.B) member this.GetPixel(x,y) = getPixel x y interface IDisposable with member this.Dispose() = bitmap.UnlockBits(data)
This only supports 24 bit images but you could easily add support for others. Bob's page gives some example's of the pointer arithmetic you would use for different images.
Then a couple of convenience type extensions for working with Bitmap:
module BitmapExtensions = open System open System.Drawing open System.IO open System.Windows open System.Windows.Media open System.Windows.Media.Imaging open System.Windows.Interop type Bitmap with member this.GetXYCoordinates = Seq.cartesianProduct {0..this.Width - 1} {0..this.Height - 1} member this.GetYXCoordinates = Seq.cartesianProduct {0..this.Height - 1} {0..this.Width - 1} member this.GetLockcontext = new LockContext(this) member this.DisplayInWindow = let bitmap = Imaging.CreateBitmapSourceFromHBitmap(this.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight(this.Width, this.Height)) let visualizer = new Window() visualizer.Title <- "Image" visualizer.Background <- new ImageBrush(bitmap) visualizer.Show() let app = new Application() app.Run(visualizer) |> ignore
Now an example of using the lock context:
let image = new Bitmap(@"D:\temp\someimage.TIF") using (new LockContext(image)) (fun lockContext -> image.GetXYCoordinates |> Seq.iter (fun (x,y) -> let color = lockContext.GetPixel(x, y) lockContext.SetPixel(x, y, color))) image.DisplayInWindow
The example above simply reads the color and rewrites it to memory.