Kyle B.
Kyle B.

Reputation: 5787

Remove surrounding whitespace from an image

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.

Upvotes: 30

Views: 44805

Answers (10)

Senad Mulaosmanović
Senad Mulaosmanović

Reputation: 23

@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

Upvotes: 0

Jonesie
Jonesie

Reputation: 7285

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

  }
}

Upvotes: -1

Trung
Trung

Reputation: 2019

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

Upvotes: 2

Claudiu
Claudiu

Reputation: 229361

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.

Upvotes: 5

user6064120
user6064120

Reputation: 61

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

Upvotes: 6

Bevan
Bevan

Reputation: 44307

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.

Upvotes: 8

Brian H
Brian H

Reputation: 4110

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
}

Upvotes: 11

Darren
Darren

Reputation: 9499

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

Upvotes: 51

Dmitri Nesteruk
Dmitri Nesteruk

Reputation: 23789

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

Upvotes: 18

Alnitak
Alnitak

Reputation: 339816

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

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

Upvotes: 1

Related Questions