【问题标题】:Remove surrounding whitespace from an image从图像中删除周围的空白
【发布时间】:2008-10-29 19:42:20
【问题描述】:

我有一组从客户那里收到的产品图片。每个产品图像都是某物的图片,并且是在白色背景下拍摄的。我想裁剪图像的所有周围部分,但只在中间留下产品。这可能吗?

例如:[http://www.5dnet.de/media/catalog/product/d/r/dress_shoes_5.jpg][1]

我不希望删除所有白色像素,但是我确实希望裁剪图像,以便最顶部的像素行包含一个非白色像素,最左侧的垂直像素行包含一个非白色像素,最底部的水平行像素包含一个非白色像素,等等。

C# 或 VB.net 中的代码将不胜感激。

【问题讨论】:

    标签: c# vb.net graphics


    【解决方案1】:

    我发现我必须调整 Dmitri 的答案以确保它适用于实际上不需要裁剪(水平、垂直或两者)的图像...

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

    【讨论】:

    • 对于带有“透明空白”的 PNG 文件,我添加了检查 bmp.GetPixel(i, row).A != 0 和 bmp.GetPixel(col, i).A != 0 ( alpha 水平为零)。现在我的 PNG 效果很好。谢谢你的代码!注意:我运行 bmp.GetPixel() 一次,然后分析 R 和 A 属性以防止重复扫描。
    • 我将添加一个需要处理的额外案例:我有一个小图像,我将使用 FillRectangle(Brushes.White,...) -> 那么我的图像将不仅有 255,因此您最好更正它。白色的值: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
    • 此解决方案存在多个错误,导致左侧和顶部出现 1 像素的白色边框。另一个用户posted a fixed version below
    • 我多年前发布了这个,我鼓励人们尝试这个稍微修改过的,如果它对你有用的话,我会更新这个答案以保持不变。 stackoverflow.com/a/36001569/329367
    【解决方案2】:

    这是我的(相当冗长的)解决方案:

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

    【讨论】:

    • 工作完美,除非croppedWidth 或croppedHeight 为零,在这种情况下,我将它们分别设置为bmp.Width 或bmp.Height,它就像一个魅力:)
    • 这对每张图片都有效吗?像 png jpeg 还是 gif?
    • @MonsterMMORPG 是的,应该,它们都被读入提供逐像素寻址的位图中。请注意,此解决方案不是很快,并且有更快的方法(在 .NET 之外)
    【解决方案3】:

    我需要一个适用于大图像的解决方案(GetPixel 很慢),所以我在下面编写了扩展方法。它似乎在我有限的测试中运行良好。缺点是必须在项目中勾选“允许不安全代码”。

    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
    }
    

    【讨论】:

    • 如果您只想修剪透明度,请将上面的“if ((cropColor.A > 0...) {”行修改为简单的“if (a != 0) {”。然后你也可以去掉cropColor变量。到目前为止效果很好。
    • 允许不安全代码是什么意思
    • @MonsterMMORPG 您可以在msdn.microsoft.com/en-us/library/ct597kb0.aspx 详细了解有关 unsafe 标志以及如何在 Visual Studio 中为您的项目打开它的更多信息。它基本上允许您使用算术等传统意义上的指针。
    • 正是我在快速周转项目中寻找的东西。谢谢!
    【解决方案4】:

    我自己编写了代码来完成这项工作 - 掌握基础知识并不难。

    本质上,您需要扫描像素行/列以检查非白色像素并隔离产品图像的边界,然后创建仅包含该区域的新位图。

    请注意,虽然Bitmap.GetPixel() 方法有效,但它相对较慢。如果处理时间很重要,您需要使用Bitmap.LockBits() 将位图锁定在内存中,然后在unsafe { } 块内使用一些简单的指针来直接访问像素。

    CodeProject 上的This article 提供了一些您可能会觉得有用的详细信息。

    【讨论】:

    • 处理 1000x1000 图像是否需要太多时间?请指教。
    • 取决于您对“太多时间”的定义 - 这取决于您的上下文。我建议使用Bitmap.GetPixel() 编写代码,然后对结果进行基准测试以查看。另请注意,智能算法比单个像素读取的微优化更重要。
    【解决方案5】:

    修复顶部和左侧剩余的 1px 空白

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

    【讨论】:

      【解决方案6】:

      这当然是可能的。在伪代码中:

      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
      

      左右两边也是如此。

      allWhiteRow 的代码将涉及查看该行中的像素并确保它们都接近到 255,255,255。

      【讨论】:

        【解决方案7】:
        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);
        }
        

        我在 ms 的其他帖子中获得了代码,但是有错误,我已经更改了一些内容,现在它运行良好。

        来自http://msm2020-sc.blogspot.com/2013/07/c-crop-white-space-from-around-image.html的帖子

        【讨论】:

          【解决方案8】:

          netpbm 图形实用程序库中的 pnmcrop 实用程序正是这样做的。

          我建议查看他们的代码,可从http://netpbm.sourceforge.net/获得

          【讨论】:

          • FWIW imagemagick 的 convert -trim 也正是这样做的 :)
          【解决方案9】:

          我复制到适用于 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);
                }
              }
          
            }
          }
          

          【讨论】: