Through the Interface: Automatically cropping a bitmap (slowly)

May 2015

Sun Mon Tue Wed Thu Fri Sat
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30


« Updated Clipboard Manager – now with preview! | Main | Automatically cropping a bitmap (more quickly) »

October 25, 2010

Automatically cropping a bitmap (slowly)

I received a few comments by email and otherwise regarding the preview capability added to the Clipboard Manager Plugin of the Month in the last post. Basically it’s useful, but only to a point: the bitmap created on the clipboard is the same size as AutoCAD’s screen, with the copied objects in their location relative to the screen. This means that if you’re working on a drawing like this:

Working on a fairly typical drawing And you want to copy the leader to the left of the cursor to the clipboard, the preview you’ll end up with will be something like this:

Our clipboard preview

Which – when scaled down to be placed at the bottom of a palette window – makes it very hard to see the geometry.

This feedback prompted me to think about auto-cropping the preview bitmap, so that we effectively “zoom in” on the geometry.

It turned out to be relatively straightforward to create a basic implementation. We loop through each pixel and check it against the background colour: if it’s not the same then we check to see whether it’s the left-/right-/bottom-/top-most, and, if so, we store it’s x and/or y coordinate to help define the limits of the cropped area.

Here are the helper functions I created to do this (in VB.NET, as that’s what the Clipboard Manager was originally written in):

  Function SameColor(ByVal a As Color, ByVal b As Color) As Boolean

    Return (a.R = b.R And a.G = b.G And a.B = b.B)

  End Function


  Function MostCommonColor(ByVal cols() As Color) As Color


    ' Use a dictionary to count our colors


    Dim cd As Dictionary(Of Color, Integer) = _

      New Dictionary(Of Color, Integer)


    ' Loop through the array, adding them one by one


    For Each col In cols


      If cd.ContainsKey(col) Then

        cd.Item(col) += 1


        cd.Add(col, 1)

      End If



    ' Now go through the dictionary and get the

    ' most popular color


    Dim max As Integer = 0

    Dim mostCommon As Color = Color.Black


    For Each kv In cd

      If kv.Value > max Then

        max = kv.Value

        mostCommon = kv.Key

      End If



    Return mostCommon


  End Function


  Function Crop(ByVal b As Bitmap, ByVal bg As Color) As Bitmap


    ' Variables for the area to crop down to


    Dim left As Integer = b.Width

    Dim top As Integer = b.Height

    Dim right As Integer = 0

    Dim bottom As Integer = 0


    ' Indeces and the current pixel


    Dim x, y As Integer

    Dim c As Color


    If bg = Nothing Then


      ' If we don't have a background passed in, get the four

      ' corners' colors and find the most common of them


      Dim cols() As Color = { _

        b.GetPixel(0, 0), _

        b.GetPixel(0, b.Height - 1), _

        b.GetPixel(b.Width - 1, 0), _

        b.GetPixel(b.Width - 1, b.Height - 1)}


      bg = MostCommonColor(cols)


    End If


    ' Loop through each pixel


    For y = 0 To b.Height - 1

      For x = 0 To b.Width - 1

        c = b.GetPixel(x, y)


        ' If it's not the same as the background color


        If Not SameColor(c, bg) Then


          ' Then we update our variables, as appropriate


          If x < left Then left = x

          If y < top Then top = y

          If x > right Then right = x

          If y > bottom Then bottom = y

        End If

      Next x

    Next y


    ' Now calculate the dimensions of the cropped output


    Dim width As Integer = (right - left)

    Dim height As Integer = (bottom - top)


    ' Add a buffer of 5% of the largest dimension (a little padding)


    Dim buffer As Integer = Math.Max(width, height) * 0.05

    width += 2 * buffer

    height += 2 * buffer


    ' Create the new bitmap and the graphics object to draw to it


    Dim cropped As New Bitmap(width, height)

    Dim gfx As Graphics = Graphics.FromImage(cropped)

    Using gfx


      ' Set the color of the bitmap to our background




      ' Draw the portion of the original image that we want to

      ' the new bitmap


      gfx.DrawImage( _

        b, New Rectangle(buffer, buffer, width, height), _

        New Rectangle(left, top, width, height), _


    End Using


    Return cropped


  End Function

One approach that was a little different: assuming no background colour is specified – which we could do, but I didn’t want to get the preferences object from AutoCAD and introduce a COM dependency on the project – we get the colours of the four corner pixels and then count the most popular. Unless very unlucky this will usually be the background color (and if we are, indeed, unlucky the image will simply not be cropped).

Now, overall, this approach to reading bitmap data is *slow* – it takes about a second to crop a preview using the above code on my system – but it does prove the concept effectively. To give it a try, here’s an updated version of the project using the above code. This one is definitely not going to be posted to Autodesk Labs: one way or another I intend to optimise this implementation, whether using Bitmap.LockBits()/UnlockBits() with lower-level memory access code or some simple file caching to make sure the copping only happens once per entry. I’m hoping that the first option will work out well, as it saves us having to clean up temporary files and is (hopefully) effective even the first time run - but we’ll see that in the next post.

At least the results are good from a visual perspective:

Cropped preview of our clipboard-resident object

blog comments powered by Disqus


10 Random Posts