Search code examples
c#imagewinapibitmapdrawing

How to copy Bitmap pixels to other Bitmap preserving alpha transparency in C#?


Could some rewrite the following function to use any optimized mechanism? I'm pretty sure that this is not the way to proceed, copying pixel by pixel.

I have read about AlphaBlend, or BitBlt, but I'm not used to native code.

public static Bitmap GetAlphaBitmap(Bitmap srcBitmap)
{
    Bitmap result = new Bitmap(srcBitmap.Width, srcBitmap.Height, PixelFormat.Format32bppArgb);

    Rectangle bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);

    BitmapData srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);

    try
    {
        for (int y = 0; y <= srcData.Height - 1; y++)
        {
            for (int x = 0; x <= srcData.Width - 1; x++)
            {
                Color pixelColor = Color.FromArgb(
                    Marshal.ReadInt32(srcData.Scan0, (srcData.Stride * y) + (4 * x)));

                result.SetPixel(x, y, pixelColor);
            }
        }
    }
    finally
    {
        srcBitmap.UnlockBits(srcData);
    }

    return result;
}

IMPORTANT NOTE: The source image has a wrong pixel format (Format32bppRgb), so I need to adjust the alpha channel. This is the only mechanism that works for me.

The reason why the src image has a wrong pixel format is explained here.

I tried the following options without luck:

  • Creating a new image and draw the src image using the Graphics.DrawImage from src. Did not preserve the alpha.
  • Creating a new image using the Scan0 form src. Works fine, but has a problem when the GC dispose the src image (explained in this other post);

This solution is the only that really works, but I know that is not optimal. I need to know how to do it using the WinAPI or other optimal mechanism.

Thank you very much!


Solution

  • Assuming the source image does infact have 32 bits per pixel, this should be a fast enough implementation using unsafe code and pointers. The same can be achieved using marshalling, though at a performance loss of around 10%-20% if I remember correctly.

    Using native methods will most likely be faster but this should already be orders of magnitude faster than SetPixel.

    public unsafe static Bitmap Clone32BPPBitmap(Bitmap srcBitmap)
    {
        Bitmap result = new Bitmap(srcBitmap.Width, srcBitmap.Height, PixelFormat.Format32bppArgb);
    
        Rectangle bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);
        BitmapData srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);
        BitmapData resData = result.LockBits(bmpBounds, ImageLockMode.WriteOnly, result.PixelFormat);
    
         int* srcScan0 = (int*)srcData.Scan0;
         int* resScan0 = (int*)resData.Scan0;
         int numPixels = srcData.Stride / 4 * srcData.Height;
         try
         {
             for (int p = 0; p < numPixels; p++)
             {
                 resScan0[p] = srcScan0[p];
             }
         }
         finally
         {
             srcBitmap.UnlockBits(srcData);
             result.UnlockBits(resData);
         }
    
        return result;
    }
    

    Here is the safe version of this method using marshalling:

    public static Bitmap Copy32BPPBitmapSafe(Bitmap srcBitmap)
    {
        Bitmap result = new Bitmap(srcBitmap.Width, srcBitmap.Height, PixelFormat.Format32bppArgb);
    
        Rectangle bmpBounds = new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height);
        BitmapData srcData = srcBitmap.LockBits(bmpBounds, ImageLockMode.ReadOnly, srcBitmap.PixelFormat);
        BitmapData resData = result.LockBits(bmpBounds, ImageLockMode.WriteOnly, result.PixelFormat);
    
        Int64 srcScan0 = srcData.Scan0.ToInt64();
        Int64 resScan0 = resData.Scan0.ToInt64();
        int srcStride = srcData.Stride;
        int resStride = resData.Stride;
        int rowLength = Math.Abs(srcData.Stride);
        try
        {
            byte[] buffer = new byte[rowLength];
            for (int y = 0; y < srcData.Height; y++)
            {
                Marshal.Copy(new IntPtr(srcScan0 + y * srcStride), buffer, 0, rowLength);
                Marshal.Copy(buffer, 0, new IntPtr(resScan0 + y * resStride), rowLength);
            }
        }
        finally
        {
            srcBitmap.UnlockBits(srcData);
            result.UnlockBits(resData);
        }
    
        return result;
    }
    

    Edit: Your source image has a negative stride, which means the scanlines are stored upside-down in memory (only on the y axis, rows still go from left to right). This effectively means that .Scan0 returns the first pixel of the last row of the bitmap.

    As such I modified the code to copy one row at a time.

    notice: I've only modified the safe code. The unsafe code still assumes positive strides for both images!