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 :
After we place it in the existing type with these lines (from the interactive session)
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.
![]()
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
To produce:
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 … ![]()
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 <- 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 -> 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>