vb.netgraphicsbackgroundgdi+transparent

Place an image on top of another, make gradient background colors transparent


I have 2 images, I need to put one on top of the other.
The second image is taken on a pink background (simulated below) and due to the light falloff the background is more sort of a gradient.

I need to place the image on the other one and remove the background color. I would like to define a Hue-range that represents my background, and have every pixel that falls into this range removed/being transparent so that it is pasted on top as if it had a transparent background.

This is the sample image I would like to paste on any random image:

Image with gradient background

I am able to paste the image onto another image by using this:

' Draw from the source to the destination.
gr.DrawImage(fr_bm, to_rect, fr_rect, GraphicsUnit.Pixel)

(image, destination rectangle, source rectangle)

But I cannot figure out how to remove the background. Any help is greatly appreciated.


Solution

  • This is a standard Color replacement filter (simplified -> no pre Convolution, since you just want to make transparent all pixels with colors that fall within a range).
    It takes a source image, copies it to a 32Bit ARGB bitmap, then generates an identical container, used as destination bitmap.

    All colors are compared to the Color specified in the colorFrom argument and, if the Color's components are within a threshold defined by the tolerance argument, the Color is replaced with the Color specified in the colorTo argument.

    The tolerance value should be in the range (1:100) (just because Photoshop and other graphic programs do that), the ColorReplacement method normalizes this value on its own.

    Possible results:
    With the image in your example, with colorFrom set to Color.Fucsia and colorTo set to Color.Transparent, the green region is isolated with a tolerance of ~56, then all remaining traces of the outer Color disappear (along with any anti-aliasing), between 80 and 90. After that, also the green area begins to fade away. Around 95, you have a completely transparent Bitmap.

    With a colorFrom set to (255, 226, 18, 212), the same results appear at ~38, then 60 to 70 (the replacement is more subtle).
    Which means you have to pick a source color that gives better result, in your view and context.

    Try it out passing different values to the method.

    C# version:

    public Bitmap ColorReplacement(Bitmap image, Color colorFrom, Color colorTo, float tolerance)
    {
        tolerance = (byte)(255.0f / 100.0f * Math.Max(Math.Min(100.0f, tolerance), 0.1f));
        Bitmap source = new(image.Width, image.Height, PixelFormat.Format32bppArgb);
        source.SetResolution(image.HorizontalResolution, image.VerticalResolution);
        using (var g = Graphics.FromImage(source)) {
            g.PixelOffsetMode = PixelOffsetMode.Half;
            g.DrawImage(image, Point.Empty);
        }
    
        Bitmap destImage = new(source.Width, source.Height, PixelFormat.Format32bppArgb);
        destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution);
        foreach (PropertyItem item in image.PropertyItems) {
            source.SetPropertyItem(item);
            destImage.SetPropertyItem(item);
        }
    
        var dataFrom = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var dataTo = destImage.LockBits(new Rectangle(0, 0, destImage.Width, destImage.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
    
        byte[] buffer = new byte[Math.Abs(dataTo.Stride) * dataTo.Height];
        Marshal.Copy(dataFrom.Scan0, buffer, 0, buffer.Length);
    
        source.UnlockBits(dataFrom);
        int bytesPerPixel = Image.GetPixelFormatSize(source.PixelFormat) / 8;
    
        for (int pos = 0; pos < buffer.Length; pos += bytesPerPixel) {
            byte blue = buffer[pos];
            byte green = buffer[pos + 1];
            byte red = buffer[pos + 2];
    
            if ((blue < colorFrom.B + tolerance && blue > colorFrom.B - tolerance) &&
                (green < colorFrom.G + tolerance && green > colorFrom.G - tolerance) &&
                (red < colorFrom.R + tolerance && red > colorFrom.R - tolerance)) {
    
                int newBlue = colorFrom.B - blue + colorTo.B;
                int newGreen = colorFrom.G - green + colorTo.G;
                int newRed = colorFrom.R - red + colorTo.R;
    
                buffer[pos] = (byte)Math.Max(Math.Min(255, newBlue), 0);
                buffer[pos + 1] = (byte)Math.Max(Math.Min(255, newGreen), 0);
                buffer[pos + 2] = (byte)Math.Max(Math.Min(255, newRed), 0);
                buffer[pos + 3] = colorTo.A;
            }
        }
        Marshal.Copy(buffer, 0, dataTo.Scan0, buffer.Length);
        destImage.UnlockBits(dataTo);
        return destImage;
    }
    

    VB.NET version:

    Public Shared Function ColorReplacement(imageSource As Bitmap, colorFrom As Color, colorTo As Color, tolerance As Single) As Bitmap
        tolerance = CByte(255.0F / 100.0F * Math.Max(Math.Min(100.0F, tolerance), 0.1F))
    
        Dim source As New Bitmap(imageSource.Width, imageSource.Height, PixelFormat.Format32bppArgb)
        source.SetResolution(imageSource.HorizontalResolution, imageSource.VerticalResolution)
        Using g = Graphics.FromImage(source)
            g.PixelOffsetMode = PixelOffsetMode.Half
            g.DrawImage(imageSource, Point.Empty)
        End Using
    
        Dim destImage As Bitmap = New Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb)
        destImage.SetResolution(imageSource.HorizontalResolution, imageSource.VerticalResolution)
        For Each item As PropertyItem In imageSource.PropertyItems
            source.SetPropertyItem(item)
            destImage.SetPropertyItem(item)
        Next
    
        Dim dataFrom = source.LockBits(New Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb)
        Dim dataTo = destImage.LockBits(New Rectangle(0, 0, destImage.Width, destImage.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb)
    
        Dim buffer As Byte() = New Byte(Math.Abs(dataFrom.Stride) * dataFrom.Height - 1) {}
        Marshal.Copy(dataFrom.Scan0, buffer, 0, buffer.Length)
    
        source.UnlockBits(dataFrom)
        Dim bytesPerPixel As Integer = Image.GetPixelFormatSize(source.PixelFormat) \ 8
    
        For pos As Integer = 0 To buffer.Length - 1 Step bytesPerPixel
            Dim blue As Integer = buffer(pos)
            Dim green As Integer = buffer(pos + 1)
            Dim red As Integer = buffer(pos + 2)
    
            If (blue < colorFrom.B + tolerance AndAlso blue > colorFrom.B - tolerance) AndAlso
                (green < colorFrom.G + tolerance AndAlso green > colorFrom.G - tolerance) AndAlso
                (red < colorFrom.R + tolerance AndAlso red > colorFrom.R - tolerance) Then
    
                Dim newBlue As Integer = colorFrom.B - blue + colorTo.B
                Dim newGreen As Integer = colorFrom.G - green + colorTo.G
                Dim newRed As Integer = colorFrom.R - red + colorTo.R
    
                buffer(pos) = CByte(Math.Max(Math.Min(255, newBlue), 0))
                buffer(pos + 1) = CByte(Math.Max(Math.Min(255, newGreen), 0))
                buffer(pos + 2) = CByte(Math.Max(Math.Min(255, newRed), 0))
                buffer(pos + 3) = colorTo.A
            End If
        Next
        Marshal.Copy(buffer, 0, dataTo.Scan0, buffer.Length)
        destImage.UnlockBits(dataTo)
        Return destImage
    End Function