Automatically cropping a bitmap (more quickly)

I mentioned in the last post that I was looking to optimise my approach for automatic cropping of an image. It turns out that using Bitmap.LockBits() and the corresponding UnlockBits() does indeed help, although I haven't run any actual benchmarks to measure the difference in the two approaches. The disadvantage of this approach is that you're a bit more down "in the weeds" when it comes to accessing the raw data – you have to support different raw image formats, for instance (and it may well be that I've missed some important ones – it's only really by chance that I realised I would receive two different bitmap formats, depending on the colour depth of the display).

Here's the optimised version of our Crop() function (called OptiCrop() 😉 which makes use of this approach. I've added a GetPixel() helper to encapsulate some of the low-level messiness, especially when it comes to reading 16- and 32-bit colour bitmaps.

  Public Shared Function GetPixel( _

    ByVal bd As BitmapData, _

    ByVal pf As PixelFormat, _

    ByVal x As Integer, ByVal y As Integer) As Color

 

    ' A counter and the RGB values of our color

 

    Dim idx, R, G, B As Integer

 

    If pf = PixelFormat.Format16bppRgb565 Then

 

      ' Variables for the raw bytes

 

      Dim bt1, bt2 As Byte

 

      ' Our pixels span 2 bytes - read them in

 

      idx = (bd.Stride * y) + (2 * x)

      bt1 = Marshal.ReadByte(bd.Scan0, idx)

      bt2 = Marshal.ReadByte(bd.Scan0, idx + 1)

 

      ' The first five bits define R

 

      R = ((bt1 And 245) >> 3) * 255 / 31

 

      ' The next 6 define G

 

      G = (((bt1 And 7) << 3) + ((bt2 And 224) >> 5)) * 255 / 63

 

      ' And the last 5 define B

 

      B = (bt2 And 31) * 255 / 31

 

    ElseIf pf = PixelFormat.Format32bppRgb Then

 

      ' Our pixels span 4 bytes - only the first 3 are used

 

      idx = (bd.Stride * y) + (4 * x)

      R = Marshal.ReadByte(bd.Scan0, idx)

      G = Marshal.ReadByte(bd.Scan0, idx + 1)

      B = Marshal.ReadByte(bd.Scan0, idx + 2)

 

    End If

< p style="margin: 0px"> 

    Return Color.FromArgb(R, G, B)

 

  End Function

 

  Public Shared Function OptiCrop( _

    ByVal b As Bitmap, ByVal bg As Color) As Bitmap

 

    ' We only support 16- and 32-bit color bitmap encoding

 

    If b.PixelFormat <> PixelFormat.Format16bppRgb565 And _

      b.PixelFormat <> PixelFormat.Format32bppRgb Then

      Return b

    End If

 

    ' 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

 

    ' Lock the bitmap's memory for reading

 

    Dim bd As BitmapData = _

      b.LockBits( _

        New Rectangle(New Point(), b.Size), _

      ImageLockMode.ReadOnly, b.PixelFormat)

 

    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 = { _

        GetPixel(bd, b.PixelFormat, 0, 0), _

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

        GetPixel(bd, b.P
ixelFormat, b.Width - 1, 0), _

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

 

      bg = MostCommonColor(cols)

 

    End If

 

    ' Loop through each pixel

 

    For y = 0 To bd.Height - 1

      For x = 0 To bd.Width - 1

        c = GetPixel(bd, b.PixelFormat, 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

 

    ' Unlock the bitmap's memory

 

    b.UnlockBits(bd)

 

    ' 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

 

      gfx.Clear(bg)

 

      ' 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), _

        GraphicsUnit.Pixel)

    End Using

 

    Return cropped

 

  End Function

I've made a number of other changes in this version, which could/should also apply to the prior one: when no items are in the list, the preview blanks (and shrinks). I've extracted the image-related code into a separate file (leaving the Crop() function alongside OptiCrop(), so you can switch between them to see if you can spot the performance difference or even run your own benchmarks).

I've also added a check for "invalid" clipboard entries… the information AutoCAD places on the clipboard is not valid between sessions. So if you ran COPYCLIP before restarting AutoCAD and launching the CLIPBOARD command, you would previously have seen something there in the list, before you've COPYCLIPped anything in the new session. That doesn't mean it's of any use: the temporary DWG file the data relies on has been deleted when the previous AutoCAD session closed. So I implemented a little check to get the path to this temporary drawing and check whether it's actually there before adding the item to the list. This same function will also help for when we implement the "Export to DWG" command in the next post.

Anyway, here's the updated project. Please post a comment if you are comparing the two versions and see a performance difference or not. And be sure also to let me know if you hit any problems, of course.

7 responses to “Automatically cropping a bitmap (more quickly)”

  1. Hi Kean - I like the changes you've made with this version. Although the copyclip speed has increased, it's not as significant for me (being that your previous version didn't slow me down any). However, the ability to copyclip block entities, and the "invalid" check are really great enhancements. Now for a new opportunity (I didn't want to say problem). I’ve noticed that the clipboard manager doesn't really like it when I copyclip text or mtext entities. And what I mean is the actual internal alpha/numeric text components when "editing" text or mtext. When any such item is copied the following happens: (1) No component is registered in the clipboard (2) The previously highlighted clipboard entity preview image remains visible, regardless of which clipboard entity is selected for potential use. This situation remains until an entity is actually pasted into the drawing (and I click a couple times in the clipboard manager). Please help clarify if this is currently normal behavior, or just me. If you have any questions or need any other information, feel free to send me an email. Thanks for the new post.

  2. With me is always the first to insert the cross-hairs hang it is always the return with a reinforcement

    Bei mir bleibt immer beim erstenmal einfügen das fadenkreuz hängen, muss immer erst mit return bestädigen

  3. Yes, even for me the speed increase was less pronounced when I looked at it again. I think I may have been working with 16-bit colour depth and the switch to 32-bit. But it was still slightly quicker, even with 32-bit colour.

    We deliberately excluded partial [M]Text fragments, right from v1.0.2, as it created some strange issues with the palette.

    It seems there are now some issues with preview - I'll take a look into that.

    Kean

  4. I'm afraid I'm not very clear on the issue: can you provide some more steps to reproduce the problem?

    Kean

  5. Whenever I have a different icon from the selections ClipboarManager to insert it will insert the symbol not the same but I must return again to confirm the insert symbol.

    Immer wenn ich ein anders Symbol vom ClipboarManager auswähle um es einzufügen wird das symbol nicht gleich eingefügt sonder ich muss nochmal mit return bestätigen um das symbol einzufügen.

  6. I'm afraid I still don't understand the problem. Please email me a set of steps to reproduce the problem (perhaps with screenshots or a video).

    Thanks,

    Kean

  7. I sent you an e-mail

    Habe Ihnen ein E-Mail gesendet

Leave a Reply

Your email address will not be published. Required fields are marked *