Применение маски к растровому изображению и создание составного изображения во время выполнения
фон
Я пытаюсь создать 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;
}