【问题标题】:Automatically trim a bitmap to minimum size?自动将位图修剪为最小尺寸?
【发布时间】:2011-06-16 18:04:49
【问题描述】:

假设我有一个 32bpp ARGB 模式的System.Drawing.Bitmap。这是一个很大的位图,但大部分是完全透明的像素,中间有一个相对较小的图像。

什么是检测“真实”图像边界的快速算法,以便我可以裁剪掉它周围的所有透明像素?

或者,.Net 中是否已有可以用于此目的的函数?

【问题讨论】:

  • 截止是直的吗?如果是这样,从 L->R 和 T->B 读取像素会很快。
  • 如果它是方形的,您可能可以节省更多时间并在所有 4 个边上从中心向外进行二进制搜索(至少减少像素询问)
  • 嵌入的小图像中是否也可以包含透明像素?
  • 不幸的是,图像可以是任何形状。
  • 我没有看到比 O(n^2) 更好的方法

标签: c# image-processing bitmap gdi+


【解决方案1】:

基本思想是检查图像的每个像素以找到图像的顶部、左侧、右侧和底部边界。要有效地执行此操作,请不要使用 GetPixel 方法,该方法非常慢。请改用LockBits

这是我想出的实现:

static Bitmap TrimBitmap(Bitmap source)
{
    Rectangle srcRect = default(Rectangle);
    BitmapData data = null;
    try
    {
        data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        byte[] buffer = new byte[data.Height * data.Stride];
        Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
        int xMin = int.MaxValue;
        int xMax = 0;
        int yMin = int.MaxValue;
        int yMax = 0;
        for (int y = 0; y < data.Height; y++)
        {
            for (int x = 0; x < data.Width; x++)
            {
                byte alpha = buffer[y * data.Stride + 4 * x + 3];
                if (alpha != 0)
                {
                    if (x < xMin) xMin = x;
                    if (x > xMax) xMax = x;
                    if (y < yMin) yMin = y;
                    if (y > yMax) yMax = y;
                }
            }
        }
        if (xMax < xMin || yMax < yMin)
        {
            // Image is empty...
            return null;
        }
        srcRect = Rectangle.FromLTRB(xMin, yMin, xMax, yMax);
    }
    finally
    {
        if (data != null)
            source.UnlockBits(data);
    }

    Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height);
    Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height);
    using (Graphics graphics = Graphics.FromImage(dest))
    {
        graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel);
    }
    return dest;
}

它可能可以优化,但我不是 GDI+ 专家,所以这是我能做的最好的,无需进一步研究......


编辑:实际上,有一种简单的优化方法,即不扫描图像的某些部分:

  1. 从左到右扫描,直到找到不透明的像素;将 (x, y) 存入 (xMin, yMin)
  2. 从上到下扫描,直到找到不透明的像素(仅适用于 x >= xMin);将 y 存入 yMin
  3. 从右到左扫描,直到找到不透明的像素(仅适用于 y >= yMin);将 x 存入 xMax
  4. 从下往上扫描,直到找到不透明的像素(仅适用于 xMin

EDIT2:这是上述方法的实现:

static Bitmap TrimBitmap(Bitmap source)
{
    Rectangle srcRect = default(Rectangle);
    BitmapData data = null;
    try
    {
        data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        byte[] buffer = new byte[data.Height * data.Stride];
        Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);

        int xMin = int.MaxValue,
            xMax = int.MinValue,
            yMin = int.MaxValue,
            yMax = int.MinValue;

        bool foundPixel = false;

        // Find xMin
        for (int x = 0; x < data.Width; x++)
        {
            bool stop = false;
            for (int y = 0; y < data.Height; y++)
            {
                byte alpha = buffer[y * data.Stride + 4 * x + 3];
                if (alpha != 0)
                {
                    xMin = x;
                    stop = true;
                    foundPixel = true;
                    break;
                }
            }
            if (stop)
                break;
        }

        // Image is empty...
        if (!foundPixel)
            return null;

        // Find yMin
        for (int y = 0; y < data.Height; y++)
        {
            bool stop = false;
            for (int x = xMin; x < data.Width; x++)
            {
                byte alpha = buffer[y * data.Stride + 4 * x + 3];
                if (alpha != 0)
                {
                    yMin = y;
                    stop = true;
                    break;
                }
            }
            if (stop)
                break;
        }

        // Find xMax
        for (int x = data.Width - 1; x >= xMin; x--)
        {
            bool stop = false;
            for (int y = yMin; y < data.Height; y++)
            {
                byte alpha = buffer[y * data.Stride + 4 * x + 3];
                if (alpha != 0)
                {
                    xMax = x;
                    stop = true;
                    break;
                }
            }
            if (stop)
                break;
        }

        // Find yMax
        for (int y = data.Height - 1; y >= yMin; y--)
        {
            bool stop = false;
            for (int x = xMin; x <= xMax; x++)
            {
                byte alpha = buffer[y * data.Stride + 4 * x + 3];
                if (alpha != 0)
                {
                    yMax = y;
                    stop = true;
                    break;
                }
            }
            if (stop)
                break;
        }

        srcRect = Rectangle.FromLTRB(xMin, yMin, xMax, yMax);
    }
    finally
    {
        if (data != null)
            source.UnlockBits(data);
    }

    Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height);
    Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height);
    using (Graphics graphics = Graphics.FromImage(dest))
    {
        graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel);
    }
    return dest;
}

如果不透明部分很小,当然不会有显着的增益,因为它仍然会扫描大部分像素。但如果它很大,只会扫描不透明部分周围的矩形。

【讨论】:

  • 顺便说一句,我刚刚意识到有一种更简单的方法来裁剪图像,而无需使用图形:return source.Clone(srcRect, source.PixelFormat);
  • 很好的解决方案,非常有帮助,但我发现我的图像被一个像素剪掉太多了。逻辑上你的似乎是对的,但我将 Rectangle.FromLTRB 的调用改为 srcRect = Rectangle.FromLTRB(xMin, yMin, xMax + 1, yMax + 1) 现在它完美运行。
  • 感谢代码!不过,default(Rectangle) 也是错误的。 Windows 不允许使用少于 1 个像素的位图。它尝试失败,等等。
  • @Bitterblue,没关系,因为srcRect的初始值在使用前总是被覆盖。
  • @ThomasLevesque 不,它并不总是被覆盖。例如data = source.LockBits(...); 可以抛出异常,srcRect = ... 在 try-block 的最后。就像我拼错了(^^):“如果尝试失败,等等。”
【解决方案2】:

我想建议一种分而治之的方法:

  1. 在中间分割图像(例如垂直)
  2. 检查切割线上是否有不透明像素(如果有,请记住边界框的最小值/最大值)
  3. 再次垂直分割左半部分
  4. 如果切割线包含不透明像素 -> 更新边界框
  5. 如果没有,你可能可以丢弃最左边的一半(我不知道图片)
  6. 继续看左右半边(您说图像在中间某处),直到找到图像的最左边界
  7. 对右半边做同样的事情

【讨论】:

  • 我认为你的第五点是错误的:可能有几个不同的区域具有不透明的像素,所以切割线上没有不透明像素的事实并不意味着什么
  • 谢谢 bjoernz,但是是的:二分搜索并不总是适用于我的图像 - 例如,可能有两个图像由空格分隔。
猜你喜欢
  • 2010-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-24
  • 2016-07-07
  • 1970-01-01
  • 1970-01-01
  • 2019-01-11
相关资源
最近更新 更多