I recently had the need to get the background color of an image. The algorithm used to perform this is simple:
Get the image and find the color that occurs at least 3/4 of the time more than the next most occurring color in the image.
Here are some examples of images and what we would expect as a result.
The background image color is white.
The background image color is red.
The background image color is white.
Now, without further ado, here is the F# code. It should be relatively easy to follow.
let main (args: string) =
let sw = new System.Diagnostics.Stopwatch()
for i = 21 to 21 do
let img = "C:\\Temp\\ImageSamples\\" + Convert.ToString(i) + ".jpg"
// get the image
let bitmap = new System.Drawing.Bitmap(img)
// process image
let background = AboutDev.ImageProcessing.GetBackgroundColor(bitmap)
printfn "%A is: %i, %i, %i" i background.R background.G background.B
printfn "Time elapsed: %A" sw.Elapsed
module ImageProcessing = begin
let GetBackgroundColor (image:Bitmap) =
// Get a Color from RGB values
let GetColor x = Color.FromArgb(Convert.ToInt32(int16 (NativePtr.get x 0)) , Convert.ToInt32(int16 (NativePtr.get x 1)) , Convert.ToInt32(int16 (NativePtr.get x 2)))
// Check for grayscale images
let IsGrayscale (x:Color) = x.R < 128uy && x.G < 128uy && x.B < 128uy
// Create a thumbnail only if the image is more that the allowable size of 300 * 300 pixels
let ToThumbnailOrNot (b:Bitmap) =
let maxAllowedDimensions = 300
// Create a thumbnail that is sized proportionately to the original
let CreateThumbnail (b:Bitmap) =
let maxPixels = 100.0
// compute the scaling factor of the original image to our max allowed pixels
let scaling = if(b.Width > b.Height) then maxPixels / Convert.ToDouble(b.Width)
else maxPixels / Convert.ToDouble(b.Height)
// compute the size of the new image as a sequence
let size = (Convert.ToInt32(Convert.ToDouble(b.Width) * scaling), Convert.ToInt32(Convert.ToDouble(b.Height) * scaling))
// create the thumbnail
new System.Drawing.Bitmap(b.GetThumbnailImage(fst size, snd size, null, IntPtr.Zero))
if b.Width > maxAllowedDimensions && b.Height > maxAllowedDimensions then CreateThumbnail b
// Get a thumbnail of the image if it is big or use the original image
let img = ToThumbnailOrNot image
// dispose the original image because a copy was made in the previous statement
// The array that is going to contain argb values to then do counts on
let items = List<int32>()
// lockbits on image so that the image can be processed quicker using unsafe means
let bd = img.LockBits(Rectangle(0,0,img.Width,img.Height),ImageLockMode.ReadWrite,PixelFormat.Format32bppArgb)
// pointer to use to go through the image
let mutable (p:nativeptr<byte>) = NativePtr.ofNativeInt (bd.Scan0)
for i=0 to img.Height-1 do
for j=0 to img.Width-1 do
// Get the color of the [x,y] pixel
let colo = (GetColor p).ToArgb()
// add the ARGB value to our list
// move to the next pixel on the row
p <- NativePtr.add p 4
// The stride - the whole length (multiplied by four to account for the fact that we are looking at 4 byte pixels
p <- NativePtr.add p (bd.Stride - bd.Width*4)
// Unlock the image bytes
// test code to see the image we worked on
// dispose the image that we worked on
//List.ofSeq items |> Seq.countBy id |> Seq.sortBy (fun x -> (~-)(snd x) ) |> Seq.take 10 |> Seq.iter (printfn "%A")
// get the first two item that occur the most in the array as a sequence
let res = List.ofSeq items |> Seq.countBy id |> Seq.sortBy (fun x -> (~-)(snd x) )
// Check to see that we have at least 2 colors
if ( (Seq.length res) < 2) then res |> Seq.head |> fst |> Color.FromArgb
let zero = Seq.head res // first item in the sequence
let one = Seq.nth 1 res // second item in the sequence
// Get the most prominent color
let background = fst zero |> Color.FromArgb
let background2 = fst one |> Color.FromArgb
// Make sure the image is not grayscale and
// the background color occurs at least 3/4 as much as the next closest color
if( (IsGrayscale background) || (((Convert.ToDouble (snd zero)) * 0.75) < (Convert.ToDouble (snd one))))
then //printfn "Cannot determine color"
//printfn "First color is: %A, %A, %A" background.R background.G background.B
//printfn "Second color is: %A, %A, %A" background2.R background2.G background2.B
//printfn "%A, %A, %A" background.R background.G background.B
// Return the color
end // End Module
NOTE: The sample code above is just that, a sample. You will need to play with it to do your bidding.
To set this up this comparison, I had a sample size of 20 images. The original code without my algorithm optimizations took 31.62 seconds to run. The F# code took 0.78 seconds.
Optimizations I made over the original code:
1. Use of LockBits
2. Creating thumbnails of the original image to work on instead of the original image when that image is large.
During my testing, I also created an image that was 13,648 * 7,912 and it took [00:00:01.2340651] to process it in my F# code. I had to stop the original C# code running on that image after 10 min! When I implemented my algorithm in C#, I was able to get the speed of the 20 images to 7.88 seconds. Still faster in F#.
F# allowed me to play with the algorithm very easily and tweak away until I got it just right.
You have to love the power of a language that lets you focus on the algorithm rather than on the minutiae of the language.