Replace color of a image, using Lockbits

后端 未结 2 1328
情话喂你
情话喂你 2021-01-22 07:29

Before nothing, I\'ll note that I accept a C# or VB.Net solution.

I have this old code which I\'m trying to refactor to avoid the bad habbits and performance inefficienc

相关标签:
2条回答
  • 2021-01-22 08:00

    I suppose this is because I'm reading 3 bytes (RGB) instead of 4 (ARGB)

    Yes, that's the point. If you want to manipulate the raw image content, you must rely on the PixelFormat. And you must differentiate the indexed formats (8bpp or less) where pixels in the BitmapData are not colors but indices of a color palette.

    public void ChangeColor(Bitmap bitmap, Color from, Color to)
    {
        if (Image.GetPixelFormatSize(bitmap.PixelFormat) > 8)
        {
            ChangeColorHiColoredBitmap(bitmap, from, to);
            return;
        }
    
        int indexFrom = Array.IndexOf(bitmap.Palette.Entries, from);
        if (indexFrom < 0)
            return; // nothing to change
    
        // we could replace the color in the palette but we want to see an example for manipulating the pixels
        int indexTo = Array.IndexOf(bitmap.Palette.Entries, to);
        if (indexTo < 0)
            return; // destination color not found - you can search for the nearest color if you want
    
        ChangeColorIndexedBitmap(bitmap, indexFrom, indexTo);
    }
    
    private unsafe void ChangeColorHiColoredBitmap(Bitmap bitmap, Color from, Color to)
    {
        int rawFrom = from.ToArgb();
        int rawTo = to.ToArgb();
    
        BitmapData data = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadWrite, bitmap.PixelFormat);
        byte* line = (byte*)data.Scan0;
        for (int y = 0; y < data.Height; y++)
        {
            for (int x = 0; x < data.Width; x++)
            {
                switch (data.PixelFormat)
                {
                    case PixelFormat.Format24bppRgb:
                        byte* pos = line + x * 3;
                        int c24 = Color.FromArgb(pos[0], pos[1], pos[2]).ToArgb();
                        if (c24 == rawFrom)
                        {
                            pos[0] = (byte)(rawTo & 0xFF);
                            pos[1] = (byte)((rawTo >> 8) & 0xFF);
                            pos[2] = (byte)((rawTo >> 16) & 0xFF);
                        }
                        break;
                    case PixelFormat.Format32bppRgb:
                    case PixelFormat.Format32bppArgb:
                        int c32 = *((int*)line + x);
                        if (c32 == rawFrom)
                            *((int*)line + x) = rawTo;
                        break;
                    default:
                        throw new NotSupportedException(); // of course, you can do the same for other pixelformats, too
                }
            }
    
            line += data.Stride;
        }
    
        bitmap.UnlockBits(data);
    }
    
    private unsafe void ChangeColorIndexedBitmap(Bitmap bitmap, int from, int to)
    {
        int bpp = Image.GetPixelFormatSize(bitmap.PixelFormat);
        if (from < 0 || to < 0 || from >= (1 << bpp) || to >= (1 << bpp))
            throw new ArgumentOutOfRangeException();
    
        if (from == to)
            return;
    
        BitmapData data = bitmap.LockBits(
            new Rectangle(Point.Empty, bitmap.Size),
            ImageLockMode.ReadWrite,
            bitmap.PixelFormat);
    
        byte* line = (byte*)data.Scan0;
    
        // scanning through the lines
        for (int y = 0; y < data.Height; y++)
        {
            // scanning through the pixels within the line
            for (int x = 0; x < data.Width; x++)
            {
                switch (bpp)
                {
                    case 8:
                        if (line[x] == from)
                            line[x] = (byte)to;
                        break;
                    case 4:
                        // First pixel is the high nibble. From and To indices are 0..16
                        byte nibbles = line[x / 2];
                        if ((x & 1) == 0 ? nibbles >> 4 == from : (nibbles & 0x0F) == from)
                        {
                            if ((x & 1) == 0)
                            {
                                nibbles &= 0x0F;
                                nibbles |= (byte)(to << 4);
                            }
                            else
                            {
                                nibbles &= 0xF0;
                                nibbles |= (byte)to;
                            }
    
                            line[x / 2] = nibbles;
                        }
                        break;
                    case 1:
                        // First pixel is MSB. From and To are 0 or 1.
                        int pos = x / 8;
                        byte mask = (byte)(128 >> (x & 7));
                        if (to == 0)
                            line[pos] &= (byte)~mask;
                        else
                            line[pos] |= mask;
                        break;
                }
            }
    
            line += data.Stride;
        }
    
        bitmap.UnlockBits(data);
    }
    
    0 讨论(0)
  • 2021-01-22 08:11

    There are three different problems in the code you posted:

    1. You have the color component order wrong. The Bitmap class stores pixel values as integers, in little-endian format. This means that the byte order of the components is actually BGR (or BGRA for 32bpp).
    2. In VB.NET, you can't directly compare Color values. I don't know enough about VB.NET to know why that is, but I assume it's a normal language behavior related to how VB.NET treats value types. To correctly compare Color values, you need to call ToArgb(), which returns an Integer value, which can be compared directly.
    3. Your For loop uses the wrong ending value. If you only subtract 1 from the length of the array, then it is possible for the loop to run into the padding at the end of a row, but find too few bytes to successfully add 2 to the loop index and still remain within the array.

    Here's a version of your extension method that works fine for me:

    <Extension>
    Public Function ChangeColor(ByVal image As Image, ByVal oldColor As Color, ByVal newColor As Color)
        Dim newImage As Bitmap = New Bitmap(image.Width, image.Height, image.PixelFormat)
    
        Using g As Graphics = Graphics.FromImage(newImage)
            g.DrawImage(image, Point.Empty)
        End Using
    
        ' Lock the bitmap's bits.
        Dim rect As New Rectangle(0, 0, newImage.Width, newImage.Height)
        Dim bmpData As BitmapData = newImage.LockBits(rect, ImageLockMode.ReadWrite, newImage.PixelFormat)
    
        ' Get the address of the first line.
        Dim ptr As IntPtr = bmpData.Scan0
    
        ' Declare an array to hold the bytes of the bitmap. 
        Dim numBytes As Integer = (bmpData.Stride * newImage.Height)
        Dim rgbValues As Byte() = New Byte(numBytes - 1) {}
    
        ' Copy the RGB values into the array.
        Marshal.Copy(ptr, rgbValues, 0, numBytes)
    
        ' Manipulate the bitmap.
        For i As Integer = 0 To rgbValues.Length - 3 Step 3
    
            Dim testColor As Color = Color.FromArgb(rgbValues(i + 2), rgbValues(i + 1), rgbValues(i))
    
            If (testColor.ToArgb() = oldColor.ToArgb()) Then
                rgbValues(i) = newColor.B
                rgbValues(i + 1) = newColor.G
                rgbValues(i + 2) = newColor.R
            End If
    
        Next i
    
        ' Copy the RGB values back to the bitmap.
        Marshal.Copy(rgbValues, 0, ptr, numBytes)
    
        ' Unlock the bits.
        newImage.UnlockBits(bmpData)
    
        Return newImage
    
    End Function
    

    As far as this goes:

    I'm not sure how to adapt it for any source pixelformat that I can pass to the function.

    Unfortunately, the API does not directly return the bits-per-pixel or bytes-per-pixel for the bitmap. You can generalize your code to take into account the number of bytes per pixel, but you'll still have to at least map the PixelFormat value to that bytes per pixel value.

    0 讨论(0)
提交回复
热议问题