Tuesday, 8 October 2013

Removing image borders

Shrinking images by removing a constant-color border is a common problem. The process turns this:

into this:

In fact, we faced this problem with all of the images from our previous blog post. One solution is to fire up a general-purpose image manipulation program like Paint.NET and invoke a series of mystical commands on each image in turn. This blog post looks at an F# program with a minimal WPF UI that allows image files to be dragged from explorer and dropped onto our UI. All such images are automatically shrunk by removing outside edges containing pixels that are all the same color.

We begin by referencing the usual WPF assemblies (PresentationCore, PresentationFramework, System.Xaml and WindowsBase) as well as System.Drawing because GDI+ exposes a more elegant interface for manipulating images. Then we open the following namespace:

open System.Windows

The following function loads an image as a 2D array of colors:

let loadImage (file: string) =
  use image = new System.Drawing.Bitmap(file)
  Array2D.init image.Width image.Height (fun x y ->
    image.GetPixel(x, y))

Note how the use binding allows the image to be automatically disposed when it falls out of scope.

Similarly, the following function saves as image of the given dimensions with pixel colors given by the getPixel function to the given file:

let saveImage (width: int) (height: int) getPixel file =
  use shrunk = new System.Drawing.Bitmap(width, height)
  for x in 0..width-1 do
    for y in 0..height-1 do
      shrunk.SetPixel(x, y, getPixel x y)
  shrunk.Save file

Next we write a function that returns the first index into a sequence of sequences where the subsequence contains more than one value or the length of the sequence if no such subsequence exists:

let find xs =
  let f xs = Seq.length(Seq.distinct xs) <> 1
  match Seq.tryFindIndex f xs with
  | None -> Seq.length xs
  | Some idx -> idx

The core of our program is the following shrink function that loads an image, identifies the rectangle within the border and saves that part of the image back to the same file:

let shrink file =
  let original = loadImage file
  let width, height = original.GetLength 0, original.GetLength 1
  let xs = [ 0..width-1 ]
  let ys = [ 0..height-1 ]
  let row y = seq { for x in xs -> original.[x, y] }
  let column x = seq { for y in ys -> original.[x, y] }
  let x0 = Seq.map column xs |> find
  let x1 = width - find(Seq.map column (List.rev xs))
  let y0 = Seq.map row ys |> find
  let y1 = height - find(Seq.map row (List.rev ys))
  let width, height = x1 - x0, y1 - y0
  if width > 0 && height > 0 then
    saveImage width height (fun x y -> original.[x+x0, y+y0]) file

The main program then creates a window, enabled drag-and-drop and adds a callback :

[<System.STAThread>]
do
  let window = Window(Title="Shrink images")
  window.AllowDrop <- true
  window.Drop.Add(fun e ->
    if e.Data.GetDataPresent DataFormats.FileDrop then
      (e.Data.GetData DataFormats.FileDrop :?> string [])
      |> Seq.iter shrink)
  Application().Run window
  |> ignore

This program can now be used to remove borders from many images in batch.

For more articles on F#, subscribe to the F# Journal!

No comments: