.Net Trimming Whitespace in an image

In case you wanted to know, it’s not quite that difficult to trim pure white pixels from an image in .net. The crux of the problem is really getting access to the color data and understanding what you are looking at when you have it. I will be demonstrating a method using 24 bits per pixel images only, but the concepts can be applied to other types.

The first step is to produce an image example that you can work with, and hopefully then you may understand what I am speaking about when I say trimming white space. The goal is to take the image on the left and transform it to the image on the right using .net.

sample output

For the purposes of the article I used a simple picturebox control which gives you access to the image contained within.  From the following lines you can see I am setting a local variable Image to the imgSrc ( my picturebox control ) Image Property.


Dim img As Image = imgSrc.Image

I then create a bitmap object based on this image and “Lock” the bits of the image which gives us access to the color information. We need to specify the rectangle that we want to lock or look-at or manipulate as well as the pixel format from the original image.


Dim bm As New Bitmap(img)
Dim rect As New Rectangle(0, 0, img.Width, img.Height)
Dim bd As BitmapData = bm.LockBits(rect, ImageLockMode.ReadWrite, img.PixelFormat)

The next part required gaining access to the bytes themselves by marshalling the data from the native memory object. In the sample code below we calculate the number of bytes as the Stride multiplied by the height of the image ( the stride is the width of a row of bitmap that may or may not be padded for memory requirements of the video card and/or windows which may not be the same as the width in pixels of the image.) Once we know the size of the byte array we need, we allocate memory for it and copy the data.


Dim bytes As Integer = Math.Abs(bd.Stride) * img.Height
Dim ptr As IntPtr = bd.Scan0
Dim rgbValues(bytes - 1) As Byte
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes)

The following code is the test that tells us if we will be using 24 bit bitmap or not – we put the rest of the code inside the if statement. You may actually start the if statement earlier on – for example before we allocate memory so that we don’t waste the processing cycles.


If img.PixelFormat = PixelFormat.Format24bppRgb Then

End If

Now that we have an array of bytes, we need to create some variables to track the state of our algorithm. I have chosen 4 variable names which I hope to be somewhat self evident to their purpose. These variables will be set to the maximums or minimum values that they can be so that when we do checks within the code they will be adjusted appropriately. For instance, the firstNonColorX is set to a minimum, so that it will increase to the appropriate value, whereas the lastNonColorX would decrease to the appropriate value. Note that the variables actually start outside the pixel boundaries of the image.


Dim firstNonColorX As Integer = -1
Dim firstNonColorY As Integer = -1
Dim lastNonColorX As Integer = bd.Width + 1
Dim lastNonColorY As Integer = bd.Height + 1

I am going to add a couple of counter variables and a boolean that tracks wether or not we found a white pixel in a scan-line or not. A scan-line will refer to a test from left to right on a given row of the image – for example ( x of 0 and y of 0 to x of width and y of 0 ). We will scan from left to right one row at a time for this algorithm until we find our first non-white x which will denote our upper left hand corner for crop.


For y As Integer = 0 To bd.Height
If firstNonColorY = -1 Then
scancounter = y * bd.Stride
fnw = False
For x As Integer = 0 To bd.Stride - 3 Step 3
If Not rgbValues(scancounter) = 255 Or Not rgbValues(scancounter +1) = 255 Or Not rgbValues(scancounter + 2) = 255 Then
If x > 0 Then
firstNonColorX = x / 3
Else
firstNonColorX = 0
End If
fnw = True
Exit For
End If
scancounter += 3
Next
If fnw Then
firstNonColorY = y
End If

In the previous code, we are iterating through the scan-lines in the first for loop, then iterating through the pixels in a scan-line in the inner forloop.

The first if statement tests to see if we are still looking for our first white pixel in the Y  direction, wjereas the second if statement which is inside the second for loop tests for our white pixels.

Notice that the rgbValues is our byte array and we increment scancounter variable by 3 in each iteration, as well as testing the individual bytes in those three bytes by incrementing our test case by nothing, then 1, then 2. These are actually referring to the R(red)G(green)B (blue) color components of the pixels which refer to 8 bits * 3 = 24 bit color per pixel.

In the next bit of code, we have assumed finding the first non white y, and are looking for the last non-white Y. Once we have found a full white line, we exit the for loop. A special note may be taken to the purpose of scancounter = y * stride which calculates the byte position within our rgbvalues byte array based on the current scan-line.


ElseIf lastNonColorY = bd.Height + 1 Then
scancounter = y * bd.Stride
fnw = False
For x As Integer = 0 To bd.Stride - 3 Step 3
If Not rgbValues(scancounter) = 255 Or Not rgbValues(scancounter + 1) = 255 Or Not rgbValues(scancounter + 2) = 255 Then
fnw = True
Exit For
End If
scancounter += 3
Next
If Not fnw Then
lastNonColorY = y
End If
Else
Exit For
End If

The final part of our algorithm scans the first scan-line that is not all white from the right edge backwards until the first non-white pixel is found to find the bottom right edge of our image. This is based on finding our first


scancounter = ((firstNonColorY + 1) * bd.Stride) - (bd.Stride - (bd.Width * 3)) - 3
fnw = False
For x As Integer = bd.Stride - 3 To 0 Step -3
If Not rgbValues(scancounter) = 255 Or Not rgbValues(scancounter + 1) = 255 Or Not rgbValues(scancounter + 2) = 255 Then
fnw = True
If x > 0 Then
lastNonColorX = x / 3
Else
lastNonColorX = 0
End If
Exit For
End If
scancounter -= 3
Next

All that is left to do now is unlock the bits of our bitmap with the following code and use the calculated image boundaries to generate a new image.


bm.UnlockBits(bd)

In the last bit of code here, I am updating a second PictureBox image with a scaled representation of the cropped image and adjusting the form height to compensate for it.


Dim aspectRatio As Double = (lastNonColorY - firstNonColorY) / (lastNonColorX - firstNonColorX)
Dim newHeight As Integer = CInt(CDbl(imgDest.Width) * aspectRatio)

Me.Height = newHeight + SystemInformation.CaptionHeight + imgDest.Top + (Me.ClientRectangle.Height - imgDest.Height) + Me.ClientRectangle.Top
imgDest.Height = newHeight
fi = New Bitmap(imgDest.Width, newHeight)
Dim g As Graphics = Graphics.FromImage(fi)
g.DrawImage(bm, New RectangleF(0, 0, imgDest.Width, newHeight), Ne RectangleF(firstNonColorX, firstNonColorY, lastNonColorX - (firstNonColorX * 2), lastNonColorY - firstNonColorY), GraphicsUnit.Pixel)

imgDest.Image = fi

2 thoughts on “.Net Trimming Whitespace in an image

Comments are closed.