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
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);
}
There are three different problems in the code you posted:
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).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.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.