Thursday, 12 July 2012

C# Crop white space from around the image

This code will crop the image based on all white and transparent pixels from around the image

cropwhitespacecropwhitespace2

public static Bitmap CropWhiteSpace(Bitmap bmp)
{
  int w = bmp.Width;
  int h = bmp.Height;
  int white = 0xffffff;
 
  Func<int, bool> allWhiteRow = r =>
  {
    for (int i = 0; i < w; ++i)
      if ((bmp.GetPixel(i, r).ToArgb() & white) != white)
        return false;
    return true;
  };
 
  Func<int, bool> allWhiteColumn = c =>
  {
    for (int i = 0; i < h; ++i)
      if ((bmp.GetPixel(c, i).ToArgb() & white) != white)
        return false;
    return true;
  };
 
  int topmost = 0;
  for (int row = 0; row < h; ++row)
  {
    if (!allWhiteRow(row))
      break;
    topmost = row;
  }
 
  int bottommost = 0;
  for (int row = h - 1; row >= 0; --row)
  {
    if (!allWhiteRow(row))
      break;
    bottommost = row;
  }
 
  int leftmost = 0, rightmost = 0;
  for (int col = 0; col < w; ++col)
  {
    if (!allWhiteColumn(col))
      break;
    leftmost = col;
  }
 
  for (int col = w - 1; col >= 0; --col)
  {
    if (!allWhiteColumn(col))
      break;
    rightmost = col;
  }
 
  if (rightmost == 0) rightmost = w; // As reached left
  if (bottommost == 0) bottommost = h; // As reached top.
 
  int croppedWidth = rightmost - leftmost;
  int croppedHeight = bottommost - topmost;
 
  if (croppedWidth == 0) // No border on left or right
  {
    leftmost = 0;
    croppedWidth = w;
  }
 
  if (croppedHeight == 0) // No border on top or bottom
  {
    topmost = 0;
    croppedHeight = h;
  }
 
  try
  {
    var target = new Bitmap(croppedWidth, croppedHeight);
    using (Graphics g = Graphics.FromImage(target))
    {
      g.DrawImage(bmp,
        new RectangleF(0, 0, croppedWidth, croppedHeight),
        new RectangleF(leftmost, topmost, croppedWidth, croppedHeight),
        GraphicsUnit.Pixel);
    }
    return target;
  }
  catch (Exception ex)
  {
    throw new Exception(
      string.Format("Values are topmost={0} btm={1} left={2} right={3} croppedWidth={4} croppedHeight={5}", topmost, bottommost, leftmost, rightmost, croppedWidth, croppedHeight),
      ex);
  }
}
[Test]
public void Test()
{
  var inputPath = "image.png";
  var outputPath = inputPath.Replace(".png", "-out.png");
 
  var bitmap = new Bitmap(inputPath);
  var cropped = CropWhiteSpace(bitmap);
  cropped.Save(outputPath, ImageFormat.Png); 
}

7 comments:

Tedebus said...

I see that this procedure doesn't work if the image is not a rectangle, is it true?
So, why don't look only for the first not white pixel (from beginning of the bitmap) and the last one (from the end)?
From their coordinates you can simply calculate the new rectangle avoiding a lot of code!

madeinstein said...

Hi Tedebus, this works fine for any shape. It finds the first non-white pixel from top/bottom/left/right and crops the image based on this information.

Tedebus said...

Hi!
I tried your code and I saw that irregular images still contain white areas (like you show on your article, now).
If your idea is to create a PNG (that has alpha information inside) you may prefer a different approach: bitmap reconstruction based on transparent color substitution on external white areas and, then, image crop.
This may be an example of code that does it
(please note that it crop not only white color, but any bounding color)

public static System.Drawing.Bitmap ClearBitmapBorders(System.Drawing.Bitmap bitmap, int tollerance)
{
Color pixelColor;
Color referenceColor = bitmap.GetPixel(0, 0);
int X1 = bitmap.Width;
int Y1 = bitmap.Height;
int X2 = 0;
int Y2 = 0;
int row, column = 0;
for (row = 0; row < bitmap.Height; row++)
{
for (column = 0; column < bitmap.Width; column++)
{
pixelColor = bitmap.GetPixel(column, row);
if ((pixelColor.ToArgb() << 8 == referenceColor.ToArgb() << 8))
{ bitmap.SetPixel(column, row, Color.Transparent); }
else
{
X1 = column < X1 ? column : X1;
Y1 = row < Y1 ? row : Y1;
break;
}
}
for (column = bitmap.Width - 1; column >= 0; column--)
{
pixelColor = bitmap.GetPixel(column, row);
if ((pixelColor.ToArgb() << 8 == referenceColor.ToArgb() << 8))
{ bitmap.SetPixel(column, row, Color.Transparent); }
else
{
X2 = column > X2 ? column : X2;
System.Diagnostics.Debug.WriteLine(X2);
Y2 = row > Y2 ? row : Y2;
break;
}
}
}
try
{
var target = new Bitmap(X2, Y2);
using (Graphics g = Graphics.FromImage(target))
{
g.DrawImage(bitmap,
new RectangleF(0, 0, X2, Y2),
new RectangleF(X1, Y1, X2, Y2),
GraphicsUnit.Pixel);
}
return target;
}
catch
{ return null; }
}

If you try it on your image you can see that now only red area will be kept and it remains white inside.

I hope it should be useful

Tedebus said...

PS "tollerance" is not used, I posted a piece of code that I used somewhere else and I forgot to delete it.

madeinstein said...

Hi, just to explain, the purpose of this code was to remove unnecessary white space from around the image to make the image boundaries (square) close to the actual image so I can send that image in the email. I've used it after creating image from html template described here: C# Convert html page to image You're trying to go a bit further to make the remaining space outside the actual image transparent. And yes I've uploaded an extra image to show how it works for non square images.

madeinstein said...

And thanks for positing your code!

Tedebus said...

Ok! I understand now...
The matter was that I found your article on Google and I was looking especially for "C# Crop white space from around the image" (there was a discussion with my colleague) and I saw it. But I saw that it wasn't the answer to my question so I decided to write to you and explain my code.

Post a Comment