Category Archives: Functional Programming

F# Image Processing – Get Background Color

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.

image

The background image color is white.

image

The background image color is red.

image

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
image image

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#.

image

 

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.

Tagged , , , , , , ,