将图像与特定颜色匹配的常规方法是在以 R、G 和 B 为轴的 3D 环境中使用颜色之间的毕达哥拉斯距离。我有一堆用于处理图像和颜色的工具集,而且我对任何外部框架都不太熟悉,所以我只是挖掘一下我的东西并给你相关的功能。
首先,颜色替换本身。此代码会将您提供的任何颜色与有限调色板上最接近的可用颜色匹配,并返回给定数组中的索引。请注意,我省略了毕达哥拉斯距离计算的“取平方根”部分;我们不需要知道实际距离,我们只需要比较它们,并且在没有这种相当耗费 CPU 的操作的情况下也能正常工作。
public static Int32 GetClosestPaletteIndexMatch(Color col, Color[] colorPalette)
{
Int32 colorMatch = 0;
Int32 leastDistance = Int32.MaxValue;
Int32 red = col.R;
Int32 green = col.G;
Int32 blue = col.B;
for (Int32 i = 0; i < colorPalette.Length; i++)
{
Color paletteColor = colorPalette[i];
Int32 redDistance = paletteColor.R - red;
Int32 greenDistance = paletteColor.G - green;
Int32 blueDistance = paletteColor.B - blue;
Int32 distance = (redDistance * redDistance) + (greenDistance * greenDistance) + (blueDistance * blueDistance);
if (distance >= leastDistance)
continue;
colorMatch = i;
leastDistance = distance;
if (distance == 0)
return i;
}
return colorMatch;
}
现在,在高色彩图像上,必须对图像上的每个像素进行调色板匹配,但如果保证您的输入已经调色,那么您可以在调色板上进行,减少您的调色板查找每张图像只有 256 个:
Color[] colors = new Color[] {Color.Black, Color.White };
ColorPalette pal = image.Palette;
for(Int32 i = 0; i < pal.Entries.Length; i++)
{
Int32 foundIndex = ColorUtils.GetClosestPaletteIndexMatch(pal.Entries[i], colors);
pal.Entries[i] = colors[foundIndex];
}
image.Palette = pal;
就是这样;调色板上的所有颜色都替换为最接近的颜色。
请注意,Palette 属性实际上创建了一个 new ColorPalette 对象,并且不引用图像中的对象,因此代码 image.Palette.Entries[0] = Color.Blue; 不起作用,因为它只是修改那个未引用的副本。因此,调色板对象总是必须被取出、编辑然后重新分配给图像。
如果您需要将结果保存到相同的文件名there's a trick with a stream you can use,但如果您只是需要将对象的调色板更改为这两种颜色,那就是真的。
如果您不确定原始图像格式,则该过程涉及更多:
正如之前在 cmets 中提到的,GetPixel 和 SetPixel 非常慢,访问图像的底层字节效率更高。但是,除非您 100% 确定输入类型的像素格式是什么,否则您不能直接访问这些字节,因为您需要知道如何读取它们。一个简单的解决方法是让框架为您完成工作,通过在新的每像素 32 位图像上绘制现有图像:
public static Bitmap PaintOn32bpp(Image image, Color? transparencyFillColor)
{
Bitmap bp = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb);
using (Graphics gr = Graphics.FromImage(bp))
{
if (transparencyFillColor.HasValue)
using (System.Drawing.SolidBrush myBrush = new System.Drawing.SolidBrush(Color.FromArgb(255, transparencyFillColor.Value)))
gr.FillRectangle(myBrush, new Rectangle(0, 0, image.Width, image.Height));
gr.DrawImage(image, new Rectangle(0, 0, bp.Width, bp.Height));
}
return bp;
}
现在,您可能希望确保透明像素最终不会因为任何颜色恰好隐藏在 alpha 值 0 后面,因此您最好在此函数中指定 transparencyFillColor 以提供背景以移除任何颜色源图像的透明度。
现在我们得到了高彩图像,下一步是检查图像字节,将它们转换为 ARGB 颜色,并使用我之前提供的函数将它们与调色板匹配。我建议制作一个 8 位图像,因为它们最容易以字节形式进行编辑,而且它们具有调色板这一事实使得在它们创建后替换它们的颜色变得非常容易。
无论如何,字节。大文件立即遍历不安全内存中的字节可能更有效,但我通常更喜欢将它们复制出来。当然是你的选择;如果你觉得值得,可以结合下面的两个功能直接访问。 Here's a good example for accessing the colour bytes directly.
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <returns>The raw bytes of the image</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride)
{
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data = new Byte[stride * sourceImage.Height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
sourceImage.UnlockBits(sourceData);
return data;
}
现在,您需要做的就是创建一个数组来表示您的 8 位图像,每四个字节遍历所有字节,并将您获得的颜色与调色板中的颜色相匹配。请注意,您可以永远假设一行像素的实际字节长度(步幅)等于宽度乘以每个像素的字节数。因此,虽然代码只是简单地将像素大小添加到读取偏移量以获取一行上的下一个像素,但它使用步幅跳过数据中的整行像素。
public static Byte[] Convert32BitTo8Bit(Byte[] imageData, Int32 width, Int32 height, Color[] palette, ref Int32 stride)
{
if (stride < width * 4)
throw new ArgumentException("Stride is smaller than one pixel line!", "stride");
Byte[] newImageData = new Byte[width * height];
for (Int32 y = 0; y < height; y++)
{
Int32 inputOffs = y * stride;
Int32 outputOffs = y * width;
for (Int32 x = 0; x < width; x++)
{
// 32bppArgb: Order of the bytes is Alpha, Red, Green, Blue, but
// since this is actually in the full 4-byte value read from the offset,
// and this value is considered little-endian, they are actually in the
// order BGRA. Since we're converting to a palette we ignore the alpha
// one and just give RGB.
Color c = Color.FromArgb(imageData[inputOffs + 2], imageData[inputOffs + 1], imageData[inputOffs]);
// Match to palette index
newImageData[outputOffs] = (Byte)ColorUtils.GetClosestPaletteIndexMatch(c, palette);
inputOffs += 4;
outputOffs++;
}
}
stride = width;
return newImageData;
}
现在我们得到了 8 位数组。要将该数组转换为图像,您可以使用the BuildImage function I already posted on another answer。
所以最后,使用这些工具,转换代码应该是这样的:
public static Bitmap ConvertToColors(Bitmap image, Color[] colors)
{
Int32 width = image.Width;
Int32 height = image.Height;
Int32 stride;
Byte[] hiColData;
// use "using" to properly dispose of temporary image object.
using (Bitmap hiColImage = PaintOn32bpp(image, colors[0]))
hiColData = GetImageData(hiColImage, out stride);
Byte[] eightBitData = Convert32BitTo8Bit(hiColData, width, height, colors, ref stride);
return BuildImage(eightBitData, width, height, stride, PixelFormat.Format8bppIndexed, colors, Color.Black);
}
我们去吧;您的图像将转换为 8 位调色板图像,适用于您想要的任何调色板。
如果你想真正匹配黑白,然后替换颜色,那也没问题;只需使用仅包含黑色和白色的调色板进行转换,然后获取生成的位图的 Palette 对象,替换其中的颜色,然后将其分配回图像。
Color[] colors = new Color[] {Color.Black, Color.White };
Bitmap newImage = ConvertToColors(image, colors);
ColorPalette pal = newImage.Palette;
pal.Entries[0] = Color.Blue;
pal.Entries[1] = Color.Yellow;
newImage.Palette = pal;