在运行时对位图应用蒙版并生成复合图像

8

背景

我正在尝试使用C# 4.0和WPF从头开始创建一个2D瓷砖引擎。我并不打算构建地球上最伟大的2D游戏,而是试图学习如何使用.NET操作图像和图形。

目标

我正在尝试将掩码应用于位图。我的位图是彩色的,我的掩码是单色位图。无论白色出现在哪里,我都试图用透明颜色替换原始位图中相应位置的颜色。

我的目标是能够在运行时存储一组已被掩蔽的图像,以便我可以按层构建它们的组合 - 即首先是背景,然后是地图上的物品,最后是玩家头像。

到目前为止我所看过的...

我已经做了很长时间了,因此在SO上发布了我的帖子 - 以下是我到目前为止所看过的内容:

我看过这篇文章Alpha masking in c# System.Drawing?,介绍了如何使用不安全的方法通过指针算术操作来操纵图像。
还有这篇文章Create Image Mask,介绍了如何使用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,在网格中托管Image元素显示图像,该网格由listviewItem托管,在ListView中托管,最后由窗口托管(如果这有任何相关性...)
所以,总结我的方法.. 我获取一张图片和相应的蒙版。图片是彩色的,蒙版是单色的。 我将我的蒙版应用到我的图片上并缓存它。 我从缓存中构建一个复合图像;依次绘制每个图像(除了透明的部分)到一个新的位图上。
现在我快要抓狂了,因为我已经做了很久,但我只看到前景图像!我可以看到很多关于这个主题的信息,但没有必要的经验来应用那些信息来解决我的问题。
编辑:我的解决方案(感谢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个回答

3
第一个方法DoApplyMask存在以下问题:
  • if (ptrMask [4 * x] == 0)表示蒙版为黑色,在您的情况下不透明,因此您必须将alpha设置为0xFF(在您的代码中相反)
对于混合代码,您需要执行以下操作:
  • 从背景层开始,并将其渲染到输出
  • 对于每个在背景以上的图层,请执行以下操作:
    • 从输出读取像素
    • 从图层读取像素
    • 计算pixel = AlphaLayer * PixelLayer +(1 - AlphaLayer)* PixelOutput
    • 将像素写入输出
您总是最终得到最上面的图层的原因是您用顶部图层颜色或透明颜色覆盖了输出的内容。而不是写入透明颜色,您应该根据图层的alpha值混合输出和图层颜色:
 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);
 }

谢谢您抽出时间回答,我晚些时候会再试一次并告诉您我的进展情况。 - Jay
我在花了一些时间尝试应用你在答案中提出的建议后,编辑了我的问题并提供了一个解决方案。非常感谢,现在终于可以正常工作了。也许我会尝试重新设计MakeCompositeBitmap()方法,但说实话,作为一个完全的n00b,我在实现你建议的数学计算方面遇到了一些困难(我想你是指我应该使用LockBits而不是Get/SetPixel来完成这个过程?)如果你想详细说明一下像素计算公式是如何工作的,我会认真听取的。无论如何,再次感谢! - Jay
好的,它起作用了,我扩展了我的答案,不确定它是否能够编译,但你已经明白了这个意思。 - thumbmunkeys

0

像 thumbmunkeys 所说的那样,用于掩码 Black 或 White 的代码是反向的。应该是:

                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;

                }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接