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), 

     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() =

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(), 
                                                               BitmapSizeOptions.FromWidthAndHeight(this.Width, this.Height))
            let visualizer = new Window()
            visualizer.Title <- "Image"
            visualizer.Background <- new ImageBrush(bitmap)
            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 ->
        |> Seq.iter (fun (x,y) -> 
                        let color = lockContext.GetPixel(x, y)
                        lockContext.SetPixel(x, y, color)))


The example above simply reads the color and rewrites it to memory.