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
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.PixelFormat, 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.