裁剪/去除图像边缘的不必要空白区域

14

我搜索了很多方法来去除不需要的空格,但都没有找到。我只找到了一些可以用来去除黑色和白色背景空格的链接。但我的背景图片可以是任何东西。所以,如果我有这些图片:

输入图像说明

图片1

输入图像说明

我该如何提取我需要的图片部分呢?例如:

输入图像说明 输入图像说明 输入图像说明


3
首先,我会选择图像四个角的颜色,如果它们相同(或至少有3个角色相同),然后迭代每个像素,将与角色匹配的颜色变黑,将其它颜色变白。 - Umur Kontacı
您是否保证具有单色/纯色背景?背景的总面积是否始终大于所包含图片的面积?边缘周围是否始终至少有1像素的背景颜色边框? - Kal_Torak
使用示例图像非常简单,可以选择很多背景。这也引出了一个问题,为什么一开始就需要这样做呢?当您处理真实图像时,回答“背景颜色是什么?”这个问题变得更加困难。没有算法可以100%准确地回答这个问题。而且很少有完全一致的单一背景颜色的图像。 - Hans Passant
@HansPassant,我在C++中找到了一个,参见http://opencv-code.com/quick-tips/auto-cropping-image-with-arbitrary-background-color/ 但是不知道如何在C#中实现。 - Imran Qadir Baksh - Baloch
你在 Stack Overflow 上看到这个了吗:https://dev59.com/gWbWa4cB1Zd3GeqPa9VW - Simon Mourier
显示剩余5条评论
2个回答

25

这是我对你的问题的解决方案:

我声明了一个方法,该方法获取原始图像,然后通过检查提供的图像的角落来查找背景颜色,如果至少有3个角具有相似的颜色(最多10%偏移),则我们已经找到了背景颜色,然后尝试找到图像中那些与背景颜色不同的形状的边界。

找到边界后,函数将剪裁图像并将新剪裁区域作为新位图返回!

这是演示文件:下载

完整的解决方案:下载

下面是ImageProcessingTools类中的函数,进行了简化:

public class ImageHelper
{
    #region CropUnwantedBackground
    public static Bitmap CropUnwantedBackground(Bitmap bmp)
    {
        var backColor = GetMatchedBackColor(bmp);
        if (backColor.HasValue)
        {
            var bounds = GetImageBounds(bmp, backColor);
            var diffX = bounds[1].X - bounds[0].X + 1;
            var diffY = bounds[1].Y - bounds[0].Y + 1;
            var croppedBmp = new Bitmap(diffX, diffY);
            var g = Graphics.FromImage(croppedBmp);
            var destRect = new Rectangle(0, 0, croppedBmp.Width, croppedBmp.Height);
            var srcRect = new Rectangle(bounds[0].X, bounds[0].Y, diffX, diffY);
            g.DrawImage(bmp, destRect, srcRect, GraphicsUnit.Pixel);
            bmp.Dispose();
            return croppedBmp;
        }
        else
        {
            bmp.Dispose();
            return null;
        }
    }
    #endregion

    #region Private Methods

    #region GetImageBounds
    private static Point[] GetImageBounds(Bitmap bmp, Color? backColor)
    {
        //--------------------------------------------------------------------
        // Finding the Bounds of Crop Area bu using Unsafe Code and Image Proccesing
        Color c;
        int width = bmp.Width, height = bmp.Height;
        bool upperLeftPointFounded = false;
        var bounds = new Point[2];
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                c = bmp.GetPixel(x, y);
                bool sameAsBackColor = ((c.R <= backColor.Value.R * 1.1 && c.R >= backColor.Value.R * 0.9) &&
                                        (c.G <= backColor.Value.G * 1.1 && c.G >= backColor.Value.G * 0.9) &&
                                        (c.B <= backColor.Value.B * 1.1 && c.B >= backColor.Value.B * 0.9));
                if (!sameAsBackColor)
                {
                    if (!upperLeftPointFounded)
                    {
                        bounds[0] = new Point(x, y);
                        bounds[1] = new Point(x, y);
                        upperLeftPointFounded = true;
                    }
                    else
                    {
                        if (x > bounds[1].X)
                            bounds[1].X = x;
                        else if (x < bounds[0].X)
                            bounds[0].X = x;
                        if (y >= bounds[1].Y)
                            bounds[1].Y = y;
                    }
                }
            }
        }
        return bounds;
    } 
    #endregion

    #region GetMatchedBackColor
    private static Color? GetMatchedBackColor(Bitmap bmp)
    {
        // Getting The Background Color by checking Corners of Original Image
        var corners = new Point[]{
            new Point(0, 0),
            new Point(0, bmp.Height - 1),
            new Point(bmp.Width - 1, 0),
            new Point(bmp.Width - 1, bmp.Height - 1)
        }; // four corners (Top, Left), (Top, Right), (Bottom, Left), (Bottom, Right)
        for (int i = 0; i < 4; i++)
        {
            var cornerMatched = 0;
            var backColor = bmp.GetPixel(corners[i].X, corners[i].Y);
            for (int j = 0; j < 4; j++)
            {
                var cornerColor = bmp.GetPixel(corners[j].X, corners[j].Y);// Check RGB with some offset
                if ((cornerColor.R <= backColor.R * 1.1 && cornerColor.R >= backColor.R * 0.9) &&
                    (cornerColor.G <= backColor.G * 1.1 && cornerColor.G >= backColor.G * 0.9) &&
                    (cornerColor.B <= backColor.B * 1.1 && cornerColor.B >= backColor.B * 0.9))
                {
                    cornerMatched++;
                }
            }
            if (cornerMatched > 2)
            {
                return backColor;
            }
        }
        return null;
    }  
    #endregion
    
    #endregion
}

这里是在ASP.NET中使用的简单示例:

if (IsPostBack && Request.Files.Count > 0)
{
    var file = Request.Files[0];
    var bmp = new Bitmap(file.InputStream);
    var croppedBmp = ImageHelper.CropUnwantedBackground(bmp);
    Response.ContentType = file.ContentType;
    croppedBmp.Save(Response.OutputStream, ImageFormat.Jpeg);
    Response.End();
}

最后我要提到的是,这些奇妙的教程对我的图像处理帮助很大:

C# 和 GDI+入门级图像处理

使用C#进行图像处理

希望对你有所帮助 :)


@Mehran提供的链接无法使用。您是否仍然拥有可下载的解决方案? - LP13
@LP13 幸运的是,我找到了解决方案!这是可下载解决方案的链接:https://www.dropbox.com/s/wpf78g3lmzq3jg4/Demo.rar?dl=0 - Mehran

4

这里有一种更可靠的方法,它使用Sobel能量滤波器和快速边界框检测程序(从WhiteSpaceTrimmer插件中提取),用于ImageResizer

namespace ImageResizer.Plugins.WhitespaceTrimmer {
public class BoundingBoxFinder {

    /// <summary>
    /// Returns a rectangle inside 'lookInside' that bounds any energy greater than 'threshold'. 
    /// </summary>
    /// <param name="image"></param>
    /// <param name="lookInside">A rectangle of 'image' to look inside. </param>
    /// <param name="threshold">1-255, the energy threshold to detect activity. 80-150 is a good range.</param>
    /// <returns></returns>
    public Rectangle FindBoxSobel(Bitmap originalImage, Rectangle lookInside, byte threshold) {

        Bitmap image = originalImage;
        try {
            //Convert if needed (makes an extra copy)
            if (image.PixelFormat != PixelFormat.Format24bppRgb &&
                image.PixelFormat != PixelFormat.Format32bppArgb &&
                image.PixelFormat != PixelFormat.Format32bppRgb) {
                image = AForge.Imaging.Image.Clone(image, PixelFormat.Format24bppRgb);
            }

            //Crop if needed (makes an extra copy unless we converted too, then only 1 extra copy)
            if (!lookInside.Equals(new Rectangle(0, 0, image.Width, image.Height))) {
                Bitmap oldImage = image;
                try {
                    image = new Crop(lookInside).Apply(image);
                } finally {
                    if (oldImage != originalImage) oldImage.Dispose(); //Dispose the cloned 
                }
            }


            //Makes 1 more copy at 1/3rd the size, in grayscale
            Rectangle result = FindBoxSobel(image, threshold);
            return new Rectangle(lookInside.X + result.X, lookInside.Y + result.Y, result.Width, result.Height);


        } finally {
            if (image != originalImage) image.Dispose();
        }

    }
    /// <summary>
    /// Requires 24 bit or 32 bit (A) RGB image. 
    /// </summary>
    /// <param name="rgb"></param>
    /// <param name="threshold"></param>
    /// <returns></returns>
    public Rectangle FindBoxSobel(Bitmap rgb, byte threshold) {
        using (Bitmap gray = Grayscale.CommonAlgorithms.Y.Apply(rgb)) {

            //Apply sobel operator to grayscale image
            new SobelEdgeDetector().ApplyInPlace(gray);
            //Threshold into black and white.
            new Threshold(threshold).ApplyInPlace(gray);
            //Trim only exact black pixels
            // lock source bitmap data
            BitmapData data = gray.LockBits(new Rectangle(0, 0, gray.Width, gray.Height), ImageLockMode.ReadOnly, gray.PixelFormat);
            try {
                return FindBoxExactGrayscale(data, 0);
            } finally {
                gray.UnlockBits(data);
            }
        }
    }
    /// <summary>
    /// Returns a bounding box that only excludes the specified color. 
    /// Only works on 8-bit images.
    /// </summary>
    /// <param name="sourceData"></param>
    /// <param name="colorToRemove">The palette index to remove.</param>
    /// <returns></returns>
    public Rectangle FindBoxExactGrayscale(BitmapData sourceData, byte indexToRemove) {
        if (sourceData.PixelFormat != PixelFormat.Format8bppIndexed) throw new ArgumentOutOfRangeException("FindBoxExact only operates on 8-bit grayscale images");
        // get source image size
        int width = sourceData.Width;
        int height = sourceData.Height;
        int offset = sourceData.Stride - width;

        int minX = width;
        int minY = height;
        int maxX = 0;
        int maxY = 0;

        // find rectangle which contains something except color to remove
        unsafe {
            byte* src = (byte*)sourceData.Scan0;

            for (int y = 0; y < height; y++) {
                if (y > 0) src += offset; //Don't adjust for offset until after first row
                for (int x = 0; x < width; x++) {
                    if (x > 0 || y > 0) src++; //Don't increment until after the first pixel.
                    if (*src != indexToRemove) {
                        if (x < minX)
                            minX = x;
                        if (x > maxX)
                            maxX = x;
                        if (y < minY)
                            minY = y;
                        if (y > maxY)
                            maxY = y;
                    }
                }
            }
        }

        // check
        if ((minX == width) && (minY == height) && (maxX == 0) && (maxY == 0)) {
            minX = minY = 0;
        }

        return new Rectangle(minX,minY,maxX - minX + 1, maxY - minY + 1);
    }
}
}

2
谢谢。我会检查这个的。但是我认为对于简单的用法,我应该使用不依赖于任何库的GDI+方法。 - Imran Qadir Baksh - Baloch
我猜这取决于你是否重视准确性。 - Lilith River

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接