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.
Main.fs
open System open System.Text.RegularExpressions open System.IO open System.Windows.Forms open System.Windows.Forms.DataVisualization.Charting open System.Diagnostics [<EntryPoint>] let main (args: string[]) = let sw = new System.Diagnostics.Stopwatch() sw.Start() 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 bitmap.Dispose() sw.Stop() printfn "Time elapsed: %A" sw.Elapsed 0
ImageProcessing.fs
namespace AboutDev #light #nowarn "9" open System open Microsoft.FSharp.NativeInterop open Microsoft.FSharp.Collections open System.Drawing open System.Drawing.Imaging open System.Collections.Generic 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 else 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 //image.Dispose() // 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 items.Add(colo) // move to the next pixel on the row p <- NativePtr.add p 4 done // 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) done // Unlock the image bytes img.UnlockBits(bd) // test code to see the image we worked on //img.Save("C:\\temp\\result.jpg", System.Drawing.Imaging.ImageFormat.Jpeg) // dispose the image that we worked on img.Dispose() //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 else 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 Color.Empty else //printfn "%A, %A, %A" background.R background.G background.B // Return the color background 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.
Original Code | F# Code |
![]() |
![]() |
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.