Chance Coble

Functional Programming in Austin, Texas

Archive for February, 2008

Grayscale Image Processing in F# : Basic Operations

Posted by Chance Coble on February 18, 2008

Last time I presented a way to create a 2D array to represent a grayscale image. This time I will build the core image type with some handy functions. Namely, I would like to be able to load an image, invert its intensity values, change the orientation and get some basic statistical information. I will also add a couple of functions for book keeping purposes. We will make use of the two functions, fromBitmap and toBitmap that I constructed last time.

I mentioned the core Image type, and my reason for using an object type library deserves some attention. Using F# I have a number of choices about how to build this image processing logic. One of the most appealing to me is to build a combinator library. However, in discussions with some C#/OO programmers, I have gotten the impression that it would be good to start with an object, and work back to a deeply functional style. Also, I often make objects of my underlying functional libraries because I work with a lot of people who are accustomed to an object oriented background. In order for my libraries to make sense to them, it is best if I make sure that they can be digested by people with a C#/Java background. For the readers of the blog, I chose to lighten the functional content and then possibly rework it later into a combinator library. If you are interested in seeing the combinator library approach right away, please do leave a comment. I should also point out that this approach does not take advantage of immutability. The image is changed in place.

To start, let’s create the image type (GSImage) along with a couple of helpful member values. Namely, the default format in which the image will be saved. We will also include a static method for loading a GSImage from a file or from an existing Bitmap (remember we work with 32bpp pixel formats by default – but I encourage you to play with other formats). I have also added a property which returns the bitmap representation.


type GSImage(im:int16[,]) =
   let format = Imaging.ImageFormat.Bmp
   static member FromBitmap(b:Bitmap) =
       let barr = fromBitmap(b)
       new GSImage(barr)
   static member FromFile(str:string) =
       use b = new Bitmap(str)
       GSImage.FromBitmap(b)
   member self.Bitmap
            with get () =  toBitmap(self.im)
   /// The Height of the image
   member self.Height = self.im.GetLength(0)
   /// The Width of the image
   member self.Width = self.im.GetLength(1)
 end

We can now start playing with our simple library. To review, placing an image in this library will just convert it to grayscale.
Our image :
Original WBAfter we place it in the existing type with these lines (from the interactive session)

> let i = GSImage.FromFile(@”C:\users\chance\pictures20wb.jpg”);;val i : GSImage

And we save it.

> i.Bitmap.Save(@”C:\users\chance\pictures\wbgs.jpg”,System.Drawing.Imaging.ImageFormat.Jpeg);; val it : unit = ()

We wind up with the following image.
Grayscale WB

Now, how hard would it be to create a function which inverts the intensity values? It’s a snap.

  /// Turn black to white and white to black
   member self.Invert() = GSImage(Array2.map (fun x -> 255s - x) self.im)

Assuming we have loaded the image into the value i, we can now use the command

> let inverted = i.Invert();;
val inverted : GSImage

To produce:

Inverted wb

And another function that turns the frog upside down. I will choose an obvious way to do this now, but later I will implement rotation as a function of the number of degrees to be rotated.


   member self.UpsideDown() =
          let arr = Array2.init (self.im.GetLength(1)) (self.im.GetLength(0))
                                (fun p q -> self.im.[q,(self.Width-1) - p])
          GSImage(arr)

Executing this operation produces … Upside Down wb
Commonly when working with images we seek descriptions of the intensity values that are more concise than observing every single value. Basic statistical measures such as the maximum, minimum and mean values are very important. When it comes to filtering, building a histogram of your data is crucial. I have added these functions below. First I use the function below to extract the basic pattern of iterating over the image and returning an aggregate.


let imFold f zero (im:int16[,]) =
     let mutable x = zero
     for i=0 to (im.GetLength(0)-1) do
       for j=0 to (im.GetLength(1)-1) do
         x <- f x (im.[i,j])
       done
     done
     x

And then I use it to extend the image object with the basic statistical properties.

   /// Get the maximum intensity value (0-255)
   member self.Max
       with get ()= imFold (fun mx x -> if(x>mx) then x else mx) 0s (self.im)
   /// Get the minimum intensity value (0-255)
   member self.Min
      with get () = imFold (fun mn x -> if(x<mn) then x else mn) Int16.MaxValue (self.im)
   /// Get the mean intensity of the image (helpful for threshoding operations)
   member self.Mean
      with get () =
          let sum = imFold (fun mean x -> (int x) + mean) 0 (self.im)
          (float sum ) / (float (self.Height * self.Width))
   /// Get the histogram of the data as an array of x values
   member self.Histogram(x:int) =
           let arr = Array.create (x) 0
           let incrBin v =
                let num = int ((float (v) / float 255) * float (x-1))
                arr.[num] <- arr.[num] + 1
                arr
           imFold (fun bins x -> incrBin x) arr (self.im)

Now we can get that information using the commands below.
> i.Mean;;

val it : float = 27.64341505

> i.Min;;

val it : int16 = 0s

> i.Max;;

val it : int16 = 255s

> i.Histogram(20);;

val it : int array

= [|97168; 813; 563; 495; 483; 538; 630; 767; 890; 1190; 1586; 1858; 2287;

2378; 2045; 2674; 1199; 508; 223; 5|]

Note how concise this code becomes as a result of the addition of our higher order function.

We already have many basic image processing operations, and the code base is hovering around 120 lines. Pretty amazing.

I will be developing this library as time goes on, adding filtering and some more advanced analysis techniques. Please leave requests for anything in particular you would like to see.

Complete code listing below.


module GSImaging = begin

 open NativeImageHandler

 let imFold f zero (im:int16[,]) =
     let mutable x = zero
     for i=0 to (im.GetLength(0)-1) do
       for j=0 to (im.GetLength(1)-1) do
         x &lt;- f x (im.[i,j])
       done
     done
     x

 type GSImage(nativeImage:int16[,]) =
   let format = Imaging.ImageFormat.Bmp
   member private x.im = nativeImage
   static member FromBitmap(b:Bitmap) =
       let barr = fromBitmap(b)
       new GSImage(barr)
   static member FromFile(str:string) =
       use b = new Bitmap(str)
       GSImage.FromBitmap(b)
   /// The Height of the image
   member self.Height = self.im.GetLength(0)
   /// The Width of the image
   member self.Width = self.im.GetLength(1)
   member self.Bitmap
            with get () =  toBitmap(self.im)
  /// Turn black to white and white to black
   member self.Invert() = GSImage(Array2.map (fun x -> 255s - x) self.im)
  /// Rotate the image counter clockwise by 90 degrees
   member self.UpsideDown() =
          let arr = Array2.init (self.im.GetLength(1)) (self.im.GetLength(0))
                                (fun p q -&gt; self.im.[q,(self.Width-1) - p])
          GSImage(arr)
   /// Get the maximum intensity value (0-255)
   member self.Max
       with get ()= imFold (fun mx x -> if(x>mx) then x else mx) 0s (self.im)
   /// Get the minimum intensity value (0-255)
   member self.Min
      with get () = imFold (fun mn x -> if(x<mn) then x else mn) Int16.MaxValue (self.im)
   /// Get the mean intensity of the image (helpful for threshoding operations)
   member self.Mean
      with get () =
          let sum = imFold (fun mean x -> (int x) + mean) 0 (self.im)
          (float sum ) / (float (self.Height * self.Width))
   /// Get the histogram of the data as an array of x values
   member self.Histogram(x:int) =
           let arr = Array.create (x) 0
           let incrBin v =
                let num = int ((float (v) / float 255) * float (x-1))
                arr.[num] <- arr.[num] + 1
                arr
           imFold (fun bins x -> incrBin x) arr (self.im)

end
<span>

</span>

Posted in Coding | 2 Comments »