wpf 将蒙版应用于位图并在运行时生成合成图像

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21500040/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 10:41:25  来源:igfitidea点击:

Applying a mask to a bitmap and generating a composite image at runtime

c#wpfmasktile

提问by Jay

Background

背景

I am trying to create a 2D tile engine from scratch using c# 4.0 and WPF. I am not attempting to build the next greatest 2D game on the planet, rather I am trying to learn how to manipulate images and graphics using .NET.

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

Goal

目标

I am trying to apply a mask to a bitmap. My bitmap is in colour and my mask is a monochrome bitmap. Where ever the colour white appears, I am trying to replace the colour in the original bitmap at the corresponding location with a transparent colour.

我正在尝试将蒙版应用于位图。我的位图是彩色的,而我的蒙版是单色位图。无论何时出现白色,我都会尝试用透明色替换相应位置的原始位图中的颜色。

My goal is to be able to store a collection of images in memory which have been masked at runtime so that I can build a composite of them in layers - i.e. a background first, an item on the map on top of it, and finally a player avatar on top of that on demand.

我的目标是能够在内存中存储一​​组在运行时被屏蔽的图像,以便我可以在图层中构建它们的组合 - 即首先是背景,地图上的一个项目在它上面,最后是一个玩家头像之上的按需。

What I have looked at so far...

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

I have been at this for quite a while now, hence my posting on SO - the following details what I have looked at so far:

我在这方面已经有一段时间了,因此我在 SO 上发布了以下详细信息:

I have looked at this post Alpha masking in c# System.Drawing?which shows how to use an unsafe method to manipulate an image using pointer arithmetic.

我看过这篇文章c# System.Drawing 中的 Alpha 掩蔽?它展示了如何使用不安全的方法使用指针算法来操作图像。

Also, this post Create Image Maskwhich shows how to use SetPixel / GetPixel to swap colours.

此外,这篇文章创建图像蒙版展示了如何使用 SetPixel / GetPixel 来交换颜色。

Various MSDN pages and other ad-hoc blogs encompassing the topic.

包含该主题的各种 MSDN 页面和其他临时博客。

What I have tried...

我尝试过的...

I have tried the unsafe pointer arithmetic method: Something like this (Please be aware this has been butchered, put together again, and repeated because I have been tinkering trying to understand why this wasn't doing what I wanted it to do):

我已经尝试过不安全的指针算术方法:像这样的东西(请注意这已经被屠杀,重新组合在一起,并重复,因为我一直在试图理解为什么这没有做我想要做的事情):

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;
    }

Also tried a variation on the other method which looked a bit like this (sorry I appear to have binned the code along the way - this is part real code/ part pseudo code - just to get across the approach I was trying...)

还尝试了另一种方法的变体,它看起来有点像这样(抱歉,我似乎在此过程中对代码进行了分类 - 这是部分真实代码/部分伪代码 - 只是为了了解我正在尝试的方法......)

{
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));
        }
    }
}

My actual problem...

我的实际问题...

So the methods above yield either the colour image on a black background or a colour image on a white background (as far as I can tell!) When I try to composite the images using code like this:

所以上面的方法产生黑色背景上的彩色图像或白色背景上的彩色图像(据我所知!)当我尝试使用这样的代码合成图像时:

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;
    }

I am calling this code with the names of various layers I want composited at runtime, the background first, then the foreground to give me an image I can show on my UI. I am expecting the cached image to have a transparency set by the mask, and I am expecting this transparency to be ignored when rendering my image (and I have given up on pointer arithmetic at the moment, I just want to get it working before optimizing it!). What I end up with is the foreground image only.

我正在使用我想在运行时合成的各个层的名称调用此代码,首先是背景,然后是前景,以便为我提供可以在 UI 上显示的图像。我希望缓存的图像具有由蒙版设置的透明度,并且我希望在渲染图像时忽略此透明度(并且我目前已经放弃了指针算法,我只想在优化之前让它工作它!)。我最终得到的只是前景图像。

So - I must re-iterate again, I am totally new at this, and know that there is probably going to be a faster way of doing this - but it seems like such a simple problem and I just want to get to the bottom of it!

所以 - 我必须再次重申,我对此完全陌生,并且知道可能会有更快的方法来做到这一点 - 但这似乎是一个简单的问题,我只想深入了解它!

I am using WPF, c# 4.0, VS2013, displaying the images in an Image element which is hosted in a grid, which is hosted by a listviewItem, hosted in a ListView, which is hosted in a grid, and finally hosted by a window (if that is of any relevance at all...)

我正在使用 WPF、c# 4.0、VS2013,在托管在网格中的 Image 元素中显示图像,该元素由 listviewItem 托管,托管在 ListView 中,该 ListView 托管在网格中,最后由窗口托管(如果这有任何意义的话......)

So to summarize my approach.. I am Getting an image and a corresponding mask. The image is colour, the mask is monochrome. I am applying my mask to my image and caching it. I am building a composite image from multiple images in the cache; drawing each one in turn (apart from the transparent bits) onto a new bitmap

所以总结一下我的方法..我正在获取图像和相应的蒙版。图像是彩色的,蒙版是单色的。我正在将我的蒙版应用于我的图像并缓存它。我正在从缓存中的多个图像构建一个合成图像;依次将每个(除了透明位)绘制到新位图上

And now I am pulling my hair out, because I have been at this for a while, I'm only seeing the foreground image! I can see a lot of information on the topic but don't have the necessary experience to apply that information to create a solution to my problem.

现在我正在拔头发,因为我已经这样做了一段时间,我只看到前景图像!我可以看到很多关于该主题的信息,但没有必要的经验来应用这些信息来为我的问题创建解决方案。

Edit: My solution (thanks to thumbmunkeys for pointing out the errors in my logic:

编辑:我的解决方案(感谢thumbmunkeys 指出我逻辑中的错误:

Fixed DoApplyMask method

固定 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;
    }

Fixed MakeCompositeBitmap() method

固定 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;
    }

采纳答案by thumbmunkeys

The first method DoApplyMaskhas the following problem:

第一种方法DoApplyMask存在以下问题:

  • if (ptrMask[4*x] == 0)means the mask is black, in your case opaque, so you have to set alpha to 0xFF (its the opposite way around in your code)
  • if (ptrMask[4*x] == 0)意味着面具是黑色的,在你的情况下是不透明的,所以你必须将 alpha 设置为 0xFF (它在你的代码中相反)

For your blending code you have to do the following things:

对于您的混合代码,您必须执行以下操作:

  • start off with the background layer and render it to output
  • for every layer above the background do the following:
    • read pixel from output
    • read pixel from layer
    • calculate pixel = AlphaLayer * PixelLayer + (1 - AlphaLayer) * PixelOutput
    • Write pixel to output
  • 从背景层开始并将其渲染到输出
  • 对于背景上方的每一层,请执行以下操作:
    • 从输出中读取像素
    • 从层读取像素
    • 计算像素 = AlphaLayer * PixelLayer + (1 - AlphaLayer) * PixelOutput
    • 将像素写入输出

The reason you always end up with uppermost layer is that you overwrite whatever is in the output with the topmost layer color, or a transparent color. Instead of writing the transparent color you should blend output and layer color based on alpha value of the layer:

您总是以最上层结束的原因是您使用最上层颜色或透明颜色覆盖输出中的任何内容。您应该根据图层的 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);
 }

回答by user2098299

Like thumbmunkeys said, the code for Black or White to mask is reversed. Should be:

就像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;

                }