Автоматически обрезать растровое изображение до минимального размера?

Предположим, что у меня есть System.Drawing.Bitmap в режиме ARGB 32bpp. Это большое растровое изображение, но в основном это полностью прозрачные пиксели с относительно небольшим изображением где-то посередине.

Что такое быстрый алгоритм для обнаружения границ "реального" изображения, поэтому я могу обрезать все прозрачные пиксели вокруг него?

В качестве альтернативы, есть ли функция, которая уже используется в .Net, которую я могу использовать для этого?

Ответ 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 +, поэтому я лучше всего могу обойтись без дальнейших исследований...


EDIT: на самом деле есть простой способ его оптимизации, не сканируя некоторые части изображения:

  • сканирование слева направо, пока не найдете непрозрачный пиксель; (x, y) в (xMin, yMin)
  • сканировать сверху вниз, пока не найдете непрозрачный пиксель (только для x >= xMin); хранить y в yMin
  • сканировать влево, пока не найдете непрозрачный пиксель (только для y >= yMin); хранить x в xMax
  • сканировать снизу вверх, пока не найдете непрозрачный пиксель (только для 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;
}

Не будет значительного выигрыша, если непрозрачная часть, конечно, мала, так как она все равно будет сканировать большую часть пикселей. Но если он большой, будут проверены только прямоугольники вокруг непрозрачной части.

Ответ 2

Я хотел бы предложить подход с разделением и победой:

  • разделить изображение в середине (например, вертикально)
  • проверьте, есть ли непрозрачные пиксели на линии разреза (если это так, помните min/max для ограничивающего блока)
  • разделить левую половину снова вертикально
  • если строка разреза содержит непрозрачные пиксели → ограничивающая рамка обновления
  • Если нет, вы можете, вероятно, отказаться от самой левой половины (я не знаю фотографии)
  • продолжайте левую-правую половину (вы заявили, что изображение находится где-то посередине), пока не найдете самую левую грань изображения
  • сделать то же самое для правой половины