Применение маски к растровому изображению и создание составного изображения во время выполнения

фон

Я пытаюсь создать 2D-движок плитки с нуля, используя c# 4.0 и WPF. Я не пытаюсь построить следующую величайшую 2D-игру на планете, а пытаюсь научиться манипулировать изображениями и графикой с помощью .Сеть.

цель

Я пытаюсь применить маску к растровому изображению. Мое растровое изображение имеет цвет, а моя маска-монохромное растровое изображение. Где бы ни появился белый цвет, я пытаюсь заменить цвет в исходном растровом изображении в соответствующем месте с прозрачным цветом.

моя цель состоит в том, чтобы иметь возможность хранить коллекцию изображений в памяти, которые были замаскированы во время выполнения, чтобы я мог построить композит из них в слоях - т. е. сначала фон, элемент на карте поверх него, и, наконец, аватар игрока поверх этого по требованию.

то, что я смотрел до сих пор...

Я был в этом довольно долго, поэтому моя публикация на SO-следующие детали, на которые я смотрел до сих пор:

Я посмотрел на этот пост Альфа-маскировка в системе c#.Рисование? который показывает, как использовать небезопасный метод для управления изображением с помощью арифметики указателя.

кроме того, это сообщение Создать Маску Изображения который показывает, как использовать SetPixel / GetPixel для замены цветов.

различные страницы MSDN и другие специальные блоги, охватывающие тема.

что я пробовал...

я попробовал небезопасный арифметический метод указателя: Что-то вроде этого (обратите внимание, что это было вырезано, собрано снова и повторено, потому что я возился, пытаясь понять, почему это не делало то, что я хотел):

private static Bitmap DoApplyMask(Bitmap input, Bitmap mask)
    {
        Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
        output.MakeTransparent();
        var rect = new Rectangle(0, 0, input.Width, input.Height);
        var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
        unsafe
        {
            for (int y = 0; y < input.Height; y++)
            {
                byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
                byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
                byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
                for (int x = 0; x < input.Width; x++)
                {
                    //I think this is right - if the blue channel is 0 than all of them are which is white?
                    if (ptrMask[4*x] == 0)
                    {
                        ptrOutput[4*x] = ptrInput[4*x]; // blue
                        ptrOutput[4*x + 1] = ptrInput[4*x + 1]; // green
                        ptrOutput[4*x + 2] = ptrInput[4*x + 2]; // red
                    }
                    else
                    ptrOutput[4 * x + 3] =255;        // alpha
                }
            }
        }
        mask.UnlockBits(bitsMask);
        input.UnlockBits(bitsInput);
        output.UnlockBits(bitsOutput);


        return output;
    }

также попробовал вариант другого метода, который выглядел немного так (извините, что я, похоже, заполнил код по пути - это часть реального код / часть псевдо-кода-просто чтобы пройти через подход, который я пытался...)

{
Bitmap input...
Bitmap mask...

Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
output.MakeTransparent();
for (int x = 0; x < output.Width; x++)
{
    for (int y = 0; y < output.Height; y++)
        {
            Color pixel = mask.GetPixel(x, y);
            //Again - not sure - my thought is if any channel has colour then it isn't white - so make it transparent.
            if (pixel.B > 0)
                output.SetPixel(x, y, Color.Transparent);
            else
        {
            pixel = input.GetPixel(x,y);
            output.SetPixel(x,y, Color.FromArgb(pixel.A, pixel.R, pixel.G,pixel.B));
        }
    }
}

моя актуальная проблема...

таким образом, приведенные выше методы дают либо цветное изображение на черном фоне, либо цветное изображение на белом фоне (насколько я могу судить!) Когда я пытаюсь составить изображения, используя такой код:

public Bitmap MakeCompositeBitmap(params string[] names)
    {
        Bitmap output = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
        output.MakeTransparent();

        foreach (string name in names)
        {
    //GetImage() returns an image which I have previously masked and cached.
            Bitmap layer = GetImage(name);

                for (int x = 0; x < output.Width; x++)
                {
                    for (int y = 0; y < output.Height; y++)
                    {
                        Color pixel = layer.GetPixel(x, y);
                        if (pixel.A > 0)
                            output.SetPixel(x, y, Color.Transparent);
                        else
                            output.SetPixel(x,y, Color.FromArgb(pixel.A, pixel.R, pixel.G,pixel.B));
                    }
                }


        }

        return output;
    }

Я вызываю этот код с именами различных слоев, которые я хочу составить во время выполнения, сначала фон, затем передний план, чтобы дать мне изображение, которое я могу показать на моем UI. Я ожидаю, что кэшированное изображение будет иметь прозрачность, заданную маской, и я ожидаю, что эта прозрачность будет проигнорирована при рендеринге моего изображения (и я отказался от арифметики указателя на данный момент, я просто хочу, чтобы он работал, прежде чем оптимизировать его!). То, что я получаю, - это только изображение на переднем плане.

Итак - я должен повторить еще раз, я совершенно новичок в этом, и знаю, что, вероятно, будет более быстрый способ сделать это-но это кажется такой простой проблемой, и я просто хочу добраться до сути!

я использую WPF, c# 4.0, VS2013, отображая изображения в элементе изображения, который размещен в сетке, который размещен в listviewItem, размещен в ListView, который размещен в сетке и, наконец, размещен в окне (если это имеет какое-либо отношение вообще...)

Итак, чтобы суммировать мой подход.. Я получаю изображение и соответствующую маску. Изображение цветное, маска ... монохромный. Я применяю маску к своему изображению и кэширую его. Я создаю составное изображение из нескольких изображений в кэше; рисование каждого по очереди (кроме прозрачных битов) на новом растровом изображении

и теперь я вытаскиваю волосы, потому что я был в этом некоторое время, я вижу только изображение на переднем плане! Я вижу много информации по этой теме, но не имею необходимого опыта для применения этой информации для создания решения для моего проблема.

Edit: мое решение (спасибо thumbmunkeys за указание ошибок в моей логике:

исправлен метод DoApplyMask

private static Bitmap DoApplyMask(Bitmap input, Bitmap mask)
    {
        Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
        output.MakeTransparent();
        var rect = new Rectangle(0, 0, input.Width, input.Height);

        var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
        unsafe
        {
            for (int y = 0; y < input.Height; y++)
            {
                byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
                byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
                byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
                for (int x = 0; x < input.Width; x++)
                {
                    //I think this is right - if the blue channel is 0 than all of them are (monochrome mask) which makes the mask black
                    if (ptrMask[4 * x] == 0)
                    {
                        ptrOutput[4 * x] = ptrInput[4 * x]; // blue
                        ptrOutput[4 * x + 1] = ptrInput[4 * x + 1]; // green
                        ptrOutput[4 * x + 2] = ptrInput[4 * x + 2]; // red

                        //Ensure opaque
                        ptrOutput[4*x + 3] = 255;
                    }
                    else
                    {
                        ptrOutput[4 * x] = 0; // blue
                        ptrOutput[4 * x + 1] = 0; // green
                        ptrOutput[4 * x + 2] = 0; // red

                        //Ensure Transparent
                        ptrOutput[4 * x + 3] = 0; // alpha
                    }
                }
            }

        }
        mask.UnlockBits(bitsMask);
        input.UnlockBits(bitsInput);
        output.UnlockBits(bitsOutput);

        return output;
    }

исправлен метод MakeCompositeBitmap ()

 public Bitmap MakeCompositeBitmap(params string[] names)
    {
        Bitmap output = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
        output.MakeTransparent();

        foreach (string name in names)
        {
            Bitmap layer = GetImage(name);

            for (int x = 0; x < output.Width; x++)
            {
                for (int y = 0; y < output.Height; y++)
                {
                    Color pixel = layer.GetPixel(x, y);
                    if (pixel.A > 0) // 0 means transparent, > 0 means opaque
                        output.SetPixel(x, y, Color.FromArgb(pixel.A, pixel.R, pixel.G, pixel.B));
                }
            }
        }

        return output;
    }

2 ответов


первый способ DoApplyMask есть следующая проблема:

  • if (ptrMask[4*x] == 0) означает, что маска черная, в вашем случае непрозрачная, поэтому вам нужно установить alpha в 0xFF (его противоположный путь в вашем коде)

для вашего кода смешивания вы должны сделать следующие вещи:

  • начните с фонового слоя и визуализируйте его для вывода
  • для каждого слоя над фоном, выполните следующие действия:
    • чтение пикселей из вывод
    • чтение пикселя из слоя
    • вычислить пиксель = AlphaLayer * PixelLayer + (1-AlphaLayer) * PixelOutput
    • написать пиксель для вывода

причина, по которой вы всегда заканчиваете с самым верхним слоем, заключается в том, что вы перезаписываете все, что находится в выходе, самым верхним цветом слоя или прозрачным цветом. Вместо того, чтобы писать прозрачный цвет, вы должны смешивать выходной и цвет слоя на основе альфа-значения слой:

 for (int y = 0; y < output.Height; y++)
 {
      Color pixel = layer.GetPixel(x, y);
      Color oPixel = output.GetPixel(x, y);          

      byte a = pixel.A;
      byte ai = 0xFF - pixel.A;

      output.SetPixel(x,y, 
              Color.FromArgb(0xFF, 
                   (pixel.R *a + oPixel.R * ai)/0xFF,  
                   (pixel.G *a + oPixel.G * ai)/0xFF,  
                   (pixel.B *a + oPixel.B * ai)/0xFF);
 }

Как сказал thumbmunkeys, код для черной или белой маски отменяется. Должно быть:

                if (ptrMask[4 * x] == 0)
                {
                   ptrOutput[4 * x] = 0; // blue
                    ptrOutput[4 * x + 1] = 0; // green
                    ptrOutput[4 * x + 2] = 0; // red

                    //Ensure Transparent
                    ptrOutput[4 * x + 3] = 0; // alpha
                }
                else
                {
                    ptrOutput[4 * x] = ptrInput[4 * x]; // blue
                    ptrOutput[4 * x + 1] = ptrInput[4 * x + 1]; // green
                    ptrOutput[4 * x + 2] = ptrInput[4 * x + 2]; // red

                    //Ensure opaque
                    ptrOutput[4*x + 3] = 255;

                }