29

I have a block of product images we received from a customer. Each product image is a picture of something and it was taken with a white background. I would like to crop all the surrounding parts of the image but leave only the product in the middle. Is this possible?

As an example: [http://www.5dnet.de/media/catalog/product/d/r/dress_shoes_5.jpg][1]

I don't want all white pixels removed, however I do want the image cropped so that the top-most row of pixels contains one non-white pixel, the left-most vertical row of pixels contains one non-white pixel, bottom-most horizontal row of pixels contains one non-white pixel, etc.

Code in C# or VB.net would be appreciated.

Kyle B.
  • 5,737
  • 6
  • 39
  • 57

10 Answers10

50

I found I had to adjust Dmitri's answer to ensure it works with images that don't actually need cropping (either horizontally, vertically or both)...

    public static Bitmap Crop(Bitmap bmp)
    {
        int w = bmp.Width;
        int h = bmp.Height;

        Func<int, bool> allWhiteRow = row =>
        {
            for (int i = 0; i < w; ++i)
                if (bmp.GetPixel(i, row).R != 255)
                    return false;
            return true;
        };

        Func<int, bool> allWhiteColumn = col =>
        {
            for (int i = 0; i < h; ++i)
                if (bmp.GetPixel(col, i).R != 255)
                    return false;
            return true;
        };

        int topmost = 0;
        for (int row = 0; row < h; ++row)
        {
            if (allWhiteRow(row))
                topmost = row;
            else break;
        }

        int bottommost = 0;
        for (int row = h - 1; row >= 0; --row)
        {
            if (allWhiteRow(row))
                bottommost = row;
            else break;
        }

        int leftmost = 0, rightmost = 0;
        for (int col = 0; col < w; ++col)
        {
            if (allWhiteColumn(col))
                leftmost = col;
            else
                break;
        }

        for (int col = w - 1; col >= 0; --col)
        {
            if (allWhiteColumn(col))
                rightmost = col;
            else
                break;
        }

        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);
        }
    }
Darren
  • 9,014
  • 2
  • 39
  • 50
  • 7
    For PNG files with the 'transparent whitespace' I have added checking for bmp.GetPixel(i, row).A != 0 and bmp.GetPixel(col, i).A != 0 (alpha level of zero). Now my PNGs are working great. Thank you for the code! Note: I run bmp.GetPixel() once then analyze R and A properties to prevent double scan. – timmi4sa Nov 21 '12 at 00:11
  • I will add an extra case that need to be treated: I have a small image and I will use a `FillRectangle(Brushes.White,...)` -> then my image will have not only 255 so you should better correct it. Values for white: `W:0 H:0 a:255 b:254 g:254 r:254 W:1 H:0 a:255 b:254 g:254 r:254 W:2 H:0 a:255 b:254 g:254 r:254 W:3 H:0 a:255 b:254 g:254 r:254 W:4 H:0 a:255 b:254 g:254 r:254 W:5 H:0 a:255 b:254 g:254 r:254 W:6 H:0 a:255 b:254 g:254 r:254 W:7 H:0 a:255 b:254 g:254 r:254 W:8 H:0 a:255 b:254 g:254 r:254 W:9 H:0 a:255 b:254 g:254 r:254 W:0 H:1 a:255 b:254 g:254 r:254 W:1 H:1 a:255 b:255 g:255 r:255` – HellBaby Apr 25 '15 at 06:27
  • 1
    This solution has several off-by-one errors that lead to a 1-pixel white border on the left and top. Another user [posted a fixed version below](https://stackoverflow.com/a/36001569/238419) – BlueRaja - Danny Pflughoeft Oct 06 '19 at 12:08
  • 1
    I posted this many years ago, I would encourage people to try this slightly modified one, if it works for you bump it up this comment and I'll update this answer to be the same. https://stackoverflow.com/a/36001569/329367 – Darren Oct 08 '19 at 01:02
18

Here's my (rather lengthy) solution:

public Bitmap Crop(Bitmap bmp)
{
  int w = bmp.Width, h = bmp.Height;

  Func<int, bool> allWhiteRow = row =>
  {
    for (int i = 0; i < w; ++i)
      if (bmp.GetPixel(i, row).R != 255)
        return false;
    return true;
  };

  Func<int, bool> allWhiteColumn = col =>
  {
    for (int i = 0; i < h; ++i)
      if (bmp.GetPixel(col, i).R != 255)
        return false;
    return true;
  };

  int topmost = 0;
  for (int row = 0; row < h; ++row)
  {
    if (allWhiteRow(row))
      topmost = row;
    else break;
  }

  int bottommost = 0;
  for (int row = h - 1; row >= 0; --row)
  {
    if (allWhiteRow(row))
      bottommost = row;
    else break;
  }

  int leftmost = 0, rightmost = 0;
  for (int col = 0; col < w; ++col)
  {
    if (allWhiteColumn(col))
      leftmost = col;
    else
      break;
  }

  for (int col = w-1; col >= 0; --col)
  {
    if (allWhiteColumn(col))
      rightmost = col;
    else
      break;
  }

  int croppedWidth = rightmost - leftmost;
  int croppedHeight = bottommost - topmost;
  try
  {
    Bitmap 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}", topmost, bottommost, leftmost, rightmost),
      ex);
  }
}
Dmitri Nesteruk
  • 23,067
  • 22
  • 97
  • 166
  • 1
    works perfectly except when the croppedWidth or croppedHeight is zero, in this case I set them to the bmp.Width or bmp.Height respectively, and it works like a charm :) – Sameh Deabes Dec 25 '11 at 14:23
  • does this work for every image? like png jpeg or gif? – Furkan Gözükara Jun 29 '16 at 11:50
  • @MonsterMMORPG yes, it should, they all get read into a Bitmap that provides by-pixel addressing. please note that this solution isn't very fast and there are much faster ways of doing this (outside of .NET) – Dmitri Nesteruk Jul 06 '16 at 08:56
10

I needed a solution that worked on large images (GetPixel is slow), so I wrote the extension method below. It seems to work well in my limited testing. The drawback is that "Allow Unsafe Code" has to be checked in your project.

public static Image AutoCrop(this Bitmap bmp)
{
    if (Image.GetPixelFormatSize(bmp.PixelFormat) != 32)
        throw new InvalidOperationException("Autocrop currently only supports 32 bits per pixel images.");

    // Initialize variables
    var cropColor = Color.White;

    var bottom = 0;
    var left = bmp.Width; // Set the left crop point to the width so that the logic below will set the left value to the first non crop color pixel it comes across.
    var right = 0;
    var top = bmp.Height; // Set the top crop point to the height so that the logic below will set the top value to the first non crop color pixel it comes across.

    var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, bmp.PixelFormat);

    unsafe
    {
        var dataPtr = (byte*)bmpData.Scan0;

        for (var y = 0; y < bmp.Height; y++)
        {
            for (var x = 0; x < bmp.Width; x++)
            {
                var rgbPtr = dataPtr + (x * 4);

                var b = rgbPtr[0];
                var g = rgbPtr[1];
                var r = rgbPtr[2];
                var a = rgbPtr[3];

                // If any of the pixel RGBA values don't match and the crop color is not transparent, or if the crop color is transparent and the pixel A value is not transparent
                if ((cropColor.A > 0 && (b != cropColor.B || g != cropColor.G || r != cropColor.R || a != cropColor.A)) || (cropColor.A == 0 && a != 0))
                {
                    if (x < left)
                        left = x;

                    if (x >= right)
                        right = x + 1;

                    if (y < top)
                        top = y;

                    if (y >= bottom)
                        bottom = y + 1;
                }
            }

            dataPtr += bmpData.Stride;
        }
    }

    bmp.UnlockBits(bmpData);

    if (left < right && top < bottom)
        return bmp.Clone(new Rectangle(left, top, right - left, bottom - top), bmp.PixelFormat);

    return null; // Entire image should be cropped, so just return null
}
Brian Hasden
  • 4,050
  • 2
  • 31
  • 37
  • 2
    If you're looking to just trim transparency, modify the "if ((cropColor.A > 0...) {" line above to simply "if (a != 0) {". Then you can get rid of the cropColor variable, as well. Works great so far. – argyle Oct 07 '15 at 21:48
  • what do you mean by Allow Unsafe Code – Furkan Gözükara Jun 29 '16 at 11:51
  • @MonsterMMORPG You can fine more information on the unsafe flag and how to turn it on for your project in Visual Studio at https://msdn.microsoft.com/en-us/library/ct597kb0.aspx. It basically allows you to use pointers in a traditional sense with arithmetic and such. – Brian Hasden Jul 01 '16 at 11:36
  • Exactly what I was looking for on a quick-turnaround project. Thanks! – Jeff Hay Aug 02 '17 at 20:30
8

I've written code to do this myself - it's not too difficult to get the basics going.

Essentially, you need to scan pixel rows/columns to check for non-white pixels and isolate the bounds of the product image, then create a new bitmap with just that region.

Note that while the Bitmap.GetPixel() method works, it's relatively slow. If processing time is important, you'll need to use Bitmap.LockBits() to lock the bitmap in memory, and then some simple pointer use inside an unsafe { } block to access the pixels directly.

This article on CodeProject gives some more details that you'll probably find useful.

Bevan
  • 43,618
  • 10
  • 81
  • 133
  • Does this take too much time to process for a 1000x1000 Image? Please advice. – techno Feb 20 '16 at 10:45
  • Depends on your definition of "too much time" - which is dependent on your context. I'd suggest writing the code using `Bitmap.GetPixel()` and then benchmarking the result to see. Also note that a smart algorithm is more important than micro-optimization of individual pixel reads. – Bevan Feb 24 '16 at 02:10
6

fix remaining 1px white space at the top and left

    public Bitmap Crop(Bitmap bitmap)
    {
        int w = bitmap.Width;
        int h = bitmap.Height;

        Func<int, bool> IsAllWhiteRow = row =>
        {
            for (int i = 0; i < w; i++)
            {
                if (bitmap.GetPixel(i, row).R != 255)
                {
                    return false;
                }
            }
            return true;
        };

        Func<int, bool> IsAllWhiteColumn = col =>
        {
            for (int i = 0; i < h; i++)
            {
                if (bitmap.GetPixel(col, i).R != 255)
                {
                    return false;
                }
            }
            return true;
        };

        int leftMost = 0;
        for (int col = 0; col < w; col++)
        {
            if (IsAllWhiteColumn(col)) leftMost = col + 1;
            else break;
        }

        int rightMost = w - 1;
        for (int col = rightMost; col > 0; col--)
        {
            if (IsAllWhiteColumn(col)) rightMost = col - 1;
            else break;
        }

        int topMost = 0;
        for (int row = 0; row < h; row++)
        {
            if (IsAllWhiteRow(row)) topMost = row + 1;
            else break;
        }

        int bottomMost = h - 1;
        for (int row = bottomMost; row > 0; row--)
        {
            if (IsAllWhiteRow(row)) bottomMost = row - 1;
            else break;
        }

        if (rightMost == 0 && bottomMost == 0 && leftMost == w && topMost == h)
        {
            return bitmap;
        }

        int croppedWidth = rightMost - leftMost + 1;
        int croppedHeight = bottomMost - topMost + 1;

        try
        {
            Bitmap target = new Bitmap(croppedWidth, croppedHeight);
            using (Graphics g = Graphics.FromImage(target))
            {
                g.DrawImage(bitmap,
                    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 top={0} bottom={1} left={2} right={3}", topMost, bottomMost, leftMost, rightMost), ex);
        }
    }
user6064120
  • 61
  • 1
  • 1
5

It's certainly possible. In pseudocode:

topmost = 0
for row from 0 to numRows:
    if allWhiteRow(row): 
        topmost = row
    else:
        # found first non-white row from top
        break

botmost = 0
for row from numRows-1 to 0:
    if allWhiteRow(row): 
        botmost = row
    else:
        # found first non-white row from bottom
        break

And similarly for left and right.

The code for allWhiteRow would involve looking at the pixels in that row and making sure they're all close to 255,255,255.

Claudiu
  • 224,032
  • 165
  • 485
  • 680
2
public void TrimImage() {
    int threshhold = 250;


    int topOffset = 0;
    int bottomOffset = 0;
    int leftOffset = 0;
    int rightOffset = 0;
    Bitmap img = new Bitmap(@"e:\Temp\Trim_Blank_Image.png");


    bool foundColor = false;
    // Get left bounds to crop
    for (int x = 1; x < img.Width && foundColor == false; x++)
    {
        for (int y = 1; y < img.Height && foundColor == false; y++)
        {
            Color color = img.GetPixel(x, y);
            if (color.R < threshhold || color.G < threshhold || color.B < threshhold)
                foundColor = true;
        }
        leftOffset += 1;
    }


    foundColor = false;
    // Get top bounds to crop
    for (int y = 1; y < img.Height && foundColor == false; y++)
    {
        for (int x = 1; x < img.Width && foundColor == false; x++)
        {
            Color color = img.GetPixel(x, y);
            if (color.R < threshhold || color.G < threshhold || color.B < threshhold)
                foundColor = true;
        }
        topOffset += 1;
    }


    foundColor = false;
    // Get right bounds to crop
    for (int x = img.Width - 1; x >= 1 && foundColor == false; x--)
    {
        for (int y = 1; y < img.Height && foundColor == false; y++)
        {
            Color color = img.GetPixel(x, y);
            if (color.R < threshhold || color.G < threshhold || color.B < threshhold)
                foundColor = true;
        }
        rightOffset += 1;
    }


    foundColor = false;
    // Get bottom bounds to crop
    for (int y = img.Height - 1; y >= 1 && foundColor == false; y--)
    {
        for (int x = 1; x < img.Width && foundColor == false; x++)
        {
            Color color = img.GetPixel(x, y);
            if (color.R < threshhold || color.G < threshhold || color.B < threshhold)
                foundColor = true;
        }
        bottomOffset += 1;
    }


    // Create a new image set to the size of the original minus the white space
    //Bitmap newImg = new Bitmap(img.Width - leftOffset - rightOffset, img.Height - topOffset - bottomOffset);

    Bitmap croppedBitmap = new Bitmap(img);
    croppedBitmap = croppedBitmap.Clone(
                    new Rectangle(leftOffset - 3, topOffset - 3, img.Width - leftOffset - rightOffset + 6, img.Height - topOffset - bottomOffset + 6),
                    System.Drawing.Imaging.PixelFormat.DontCare);


    // Get a graphics object for the new bitmap, and draw the original bitmap onto it, offsetting it do remove the whitespace
    //Graphics g = Graphics.FromImage(croppedBitmap);
    //g.DrawImage(img, 1 - leftOffset, 1 - rightOffset);
    croppedBitmap.Save(@"e:\Temp\Trim_Blank_Image-crop.png", ImageFormat.Png);
}

I have got code from other post in ms, but that has bugs, I have changed something, now it works good.

The post from http://msm2020-sc.blogspot.com/2013/07/c-crop-white-space-from-around-image.html

Trung
  • 1,819
  • 1
  • 13
  • 10
1

The pnmcrop utility from the netpbm graphics utilities library does exactly that.

I suggest looking at their code, available from http://netpbm.sourceforge.net/

Alnitak
  • 334,560
  • 70
  • 407
  • 495
0

@Jonesie works great, but you have a bug with AllWhiteColumn pixel was wrong calculated var px = i * w + col; is correct. Also isTransparent should include white color SKColors.White or better compare it using rgb with offset r,g,b >200

-1

I copied to a version that works with SkiaSharp.

using SkiaSharp;
using System;

//
// Based on the original stackoverflow post:  https://stackoverflow.com/questions/248141/remove-surrounding-whitespace-from-an-image
//
namespace BlahBlah
{

  public static class BitmapExtensions
  {
    public static SKBitmap TrimWhitespace(this SKBitmap bmp)
    {
      int w = bmp.Width;
      int h = bmp.Height;
      
      // get all the pixels here - this can take a while so dont want it in the loops below
      // maybe theres a more efficient way?  loading all the pixels could be greedy
      var pixels = bmp.Pixels;

      bool IsTransparent(SKColor color)
      {
        return (color.Red == 0 && color.Green == 0 && color.Blue == 0 && color.Alpha == 0) || 
          (color == SKColors.Transparent);
      }

      Func<int, bool> allWhiteRow = row =>
      {
        for (int i = 0; i < w; ++i)
        {
          var px = row * w + i;
          if (!IsTransparent(pixels[px]))
            return false;
        }
        return true;
      };

      Func<int, bool> allWhiteColumn = col =>
      {
        for (int i = 0; i < h; ++i)
        {
          var px = col * h + i;
          if (!IsTransparent(pixels[px]))
            return false;
        }
        return true;
      };

      int topmost = 0;
      for (int row = 0; row < h; ++row)
      {
        if (allWhiteRow(row))
          topmost = row;
        else break;
      }

      int bottommost = 0;
      for (int row = h - 1; row >= 0; --row)
      {
        if (allWhiteRow(row))
          bottommost = row;
        else break;
      }

      int leftmost = 0, rightmost = 0;
      for (int col = 0; col < w; ++col)
      {
        if (allWhiteColumn(col))
          leftmost = col;
        else
          break;
      }

      for (int col = w - 1; col >= 0; --col)
      {
        if (allWhiteColumn(col))
          rightmost = col;
        else
          break;
      }

      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 SKBitmap(croppedWidth, croppedHeight);

        using var canvas = new SKCanvas(target);
        using var img = SKImage.FromBitmap(bmp);
        canvas.DrawImage(img,
          new SKRect(leftmost, topmost, rightmost, bottommost),
          new SKRect(0, 0, croppedWidth, croppedHeight));

        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);
      }
    }

  }
}
Jonesie
  • 6,997
  • 10
  • 48
  • 66