使用锁定位替换图像的颜色
本文关键字:图像 颜色 替换 定位 锁定 | 更新日期: 2023-09-27 18:20:44
在此之前,我会注意到我将接受C#或VB.NET解决方案。
我有一段旧代码,我正在尝试重构它,以避免使用GetPixel
/SetPixel
方法的坏习惯和性能低下:
<Extension>
Public Function ChangeColor(ByVal sender As Image,
ByVal oldColor As Color,
ByVal newColor As Color) As Image
Dim bmp As New Bitmap(sender.Width, sender.Height, sender.PixelFormat)
Dim x As Integer = 0
Dim y As Integer = 0
While (x < bmp.Width)
y = 0
While y < bmp.Height
If DirectCast(sender, Bitmap).GetPixel(x, y) = oldColor Then
bmp.SetPixel(x, y, newColor)
End If
Math.Max(Threading.Interlocked.Increment(y), y - 1)
End While
Math.Max(Threading.Interlocked.Increment(x), x - 1)
End While
Return bmp
End Function
因此,在阅读了使用LockBits
方法的投票率最高的解决方案后,我试图根据我的需求调整代码,使用Color
作为参数,而不是字节序列(因为本质上它们是相同的):
<Extension>
Public Function ChangeColor(ByVal sender As Image,
ByVal oldColor As Color,
ByVal newColor As Color) As Image
Dim bmp As Bitmap = DirectCast(sender.Clone, Bitmap)
' Lock the bitmap's bits.
Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height)
Dim bmpData As BitmapData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.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 * bmp.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 - 1 Step 3
If (Color.FromArgb(rgbValues(i), rgbValues(i + 1), rgbValues(i + 2)) = oldColor) Then
rgbValues(i) = newColor.R
rgbValues(i + 1) = newColor.G
rgbValues(i + 2) = newColor.B
End If
Next i
' Copy the RGB values back to the bitmap.
Marshal.Copy(rgbValues, 0, ptr, numBytes)
' Unlock the bits.
bmp.UnlockBits(bmpData)
Return bmp
End Function
我对扩展方法有两个问题:第一个问题是,如果pixelformat不是原始示例中的Format24bppRgb
,那么一切都会出错:;IndexOutOfRange";循环中抛出异常。我想这是因为我读取的是3个字节(RGB)而不是4个字节(ARGB),但我不知道如何将其适应我可以传递给函数的任何源像素格式。
第二个是,如果我按照原来的C#示例使用Format24bppRgb
,颜色会变为黑色。
请注意,我不确定我链接到的C#问题中给出的原始解决方案是否是错误的,因为根据他们的评论,这似乎在某种程度上是错误的。
这就是我尝试使用它的方式:
' This function creates a bitmap of a solid color.
Dim srcImg As Bitmap = ImageUtil.CreateSolidcolorBitmap(New Size(256, 256), Color.Red)
Dim modImg As Image = srcImg.ChangeColor(Color.Red, Color.Blue)
PictureBox1.BackgroundImage = srcImg
PictureBox2.BackgroundImage = modImg
我想这是因为我读取的是3字节(RGB)而不是4字节(ARGB)
是的,这就是重点。如果要操作原始图像内容,则必须依赖PixelFormat
。您必须区分索引格式(8ppp或更低),其中BitmapData
中的像素不是颜色,而是调色板的索引。
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);
}
您发布的代码中有三个不同的问题:
- 您的颜色组件顺序错误。
Bitmap
类以小端序格式将像素值存储为整数。这意味着组件的字节顺序实际上是BGR(或32bpp的BGRA) - 在VB.NET中,不能直接比较
Color
值。我对VB.NET的了解还不够,不知道为什么会这样,但我认为这是一种与VB.NET如何处理值类型有关的正常语言行为。要正确比较Color
的值,需要调用ToArgb()
,它返回一个Integer
的值,可以直接进行比较 - 您的
For
循环使用了错误的结束值。如果只从数组的长度中减去1
,那么循环可能会运行到行末尾的填充中,但发现字节太少,无法成功地将2
添加到循环索引中,并且仍保留在数组中
以下是您的扩展方法的一个版本,对我来说很好:
<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
就目前而言:
我不知道如何将它适应任何源像素格式,我可以将其传递给函数。
遗憾的是,API不会直接返回位图的逐位或逐字节。您可以将代码泛化为考虑每个像素的字节数,但您仍然需要至少将PixelFormat
值映射到每个像素的那个字节值。