Search code examples
c#wpf.net-7.0

Convert bitmap to gray scale and keep transparency


Can you advise me how to ensure transparency after conversion to grayscale in the case of a wpf c# application and resource image in png format programmatically?

I created a minimal working project to test, you can find it here: Github GrayTransparencyTest

Edit 2: Github repository has been updated to the first two solutions from users "Just Answer the Question" and "Clemens". Transparency is preserved even if the conversion to grayscale looks slightly different from the xaml.

The problem looks like this (now including first 2 solutions):
Result
I need the 4th image to retain the transparency of the original image after converting to grayscale.

Using XAML everything works:

<Image>
  <Image.Source>
    <FormatConvertedBitmap DestinationFormat="Gray16">
      <FormatConvertedBitmap.Source>
        <BitmapImage UriSource="pack://application:,,,/GrayTransparencyTest;component/Media/priority.png" />
      </FormatConvertedBitmap.Source>
    </FormatConvertedBitmap>
  </Image.Source>
  <Image.OpacityMask>
    <ImageBrush>
      <ImageBrush.ImageSource>
        <BitmapImage UriSource="pack://application:,,,/GrayTransparencyTest;component/Media/priority.png" />
      </ImageBrush.ImageSource>
    </ImageBrush>
  </Image.OpacityMask>
</Image>

Using binding Image and C# methods for FormatConvertedBitmap, no chance:

string imageKey = "priority";
Uri imageURI = new Uri($"pack://application:,,,/GrayTransparencyTest;component/Media/{imageKey}.png", UriKind.Absolute);
BitmapImage bitmapImage = new BitmapImage(imageURI);
ImageBrush opacityMask = new ImageBrush()
{
  ImageSource = bitmapImage
};
FormatConvertedBitmap bitmapGreyscale = new FormatConvertedBitmap();
bitmapGreyscale.BeginInit();
bitmapGreyscale.Source = new BitmapImage(imageURI);
bitmapGreyscale.DestinationFormat = PixelFormats.Gray16;
bitmapGreyscale.EndInit();
ImageGray = new Image()
{
  Source = bitmapGreyscale,
  OpacityMask = opacityMask,
};

It looks like OpacityMask is not working. How is it possible to add transparency to Image please? Can you advise if this OpacityMask = opacityMask is really not supposed to work the way I expected it to, or how I would fix it to work like it does in XAML? (workaround: grayscale versions of images direct as resources).

According to the answers so far it looks like it can't be done other than by directly editing the bitmap pixels of the image. So far no one has explained to me why the original code does not work.


Solution

  • Here is a straightforward solution without System.Drawing.Bitmap and GDI.

    It creates a new BitmapSource from the grayscale-converted buffer of the original BitmapSource that keeps the alpha channel. The code works for the usual PixelFormats.Bgra32 and PixelFormats.Pbgra32 formats, but could easily be adapted to other formats.

    public static BitmapSource ConvertToGrayscale(BitmapSource source)
    {
        var stride = (source.PixelWidth * source.Format.BitsPerPixel + 7) / 8;
        var pixels = new byte[stride * source.PixelWidth];
    
        source.CopyPixels(pixels, stride, 0);
    
        for (int i = 0; i < pixels.Length; i += 4)
        {
            // this works for PixelFormats.Bgra32
            var blue = pixels[i];
            var green = pixels[i + 1];
            var red = pixels[i + 2];
            var gray = (byte)(0.2126 * red + 0.7152 * green + 0.0722 * blue);
            pixels[i] = gray;
            pixels[i + 1] = gray;
            pixels[i + 2] = gray;
        }
    
        return BitmapSource.Create(
            source.PixelWidth, source.PixelHeight,
            source.DpiX, source.DpiY,
            source.Format, null, pixels, stride);
    }
    

    Use it like this:

    ImageGray = new Image { Source = ConvertToGrayscale(bitmapImage) };