自动将位图修剪为最小尺寸?

Blo*_*ard 11 c# gdi+ bitmap image-processing

假设我有一个System.Drawing.Bitmap32bpp的ARGB模式.这是一个很大的位图,但它主要是完全透明的像素,在中间某处有一个相对较小的图像.

什么是快速算法来检测"真实"图像的边框,所以我可以裁掉周围的所有透明像素?

或者,我可以使用.Net中的功能吗?

Tho*_*que 25

基本思想是检查图像的每个像素以找到图像的顶部,左侧,右侧和底部边界.要有效地执行此操作,请不要使用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;
}
Run Code Online (Sandbox Code Playgroud)

它可能是优化的,但我不是GDI +专家,所以如果没有进一步的研究,它是我能做的最好的...


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

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 顺便说一下,我刚刚意识到有一种更简单的方法来裁剪图像,而不使用图形:`return source.Clone(srcRect,source.PixelFormat);` (2认同)
  • 很好的解决方案,非常有帮助,但我发现我的图像被一个像素剪得太多了.从逻辑上看,你似乎没错,但是我把调用改为**Rectangle.FromLTRB**到**srcRect = Rectangle.FromLTRB(xMin,yMin,xMax + 1,yMax + 1)**现在它完美地工作了. (2认同)