应用调色板与替换调色板不同。您缺少的步骤是将图像上的实际像素与新颜色相匹配。
请看,索引图像上的像素不包含颜色。它们包含对调色板的引用。因此,如果您在图像上有一个红色像素,那不是一个值为“red”的像素,它是一个具有从 0 到 255 的某个值的像素,指的是调色板上该索引处的颜色。所以说它的值为“23”,那么这意味着它使用调色板上索引 23 上的颜色,并且该颜色是红色。
但您正在替换调色板上的这些颜色。所以很明显,在你的操作之后,引用索引 23 的那个像素将不再是它之前的红色;它将具有您替换索引 23 的任何颜色。这就是为什么图像上的所有颜色都会乱七八糟的原因。
基本上,您需要做的是查看像素使用的颜色,在新调色板上找到最接近的匹配,然后将在新调色板上找到最接近匹配的索引保存到像素中。
执行此操作的方法比您想象的要复杂一些。用于在图像上绘制的普通 .Net 函数无法访问这些原始索引值;他们只能使用颜色。所以你必须更深入一点。
由于这是一个每像素 8 位的图像,因此图像上的每个颜色索引值都是一个字节。所以你要做的第一件事就是获取这些字节。这是通过LockBits 操作完成的,它使您可以访问图像的底层内存。然后,您可以使用Marshal.Copy 来获取字节,而无需使用裸指针。
那么让我们从获取这些字节的函数开始:
/// <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;
}
重要提示:“步幅”是图像上每一行的字节数。由于这通常四舍五入为 4 字节的倍数,因此请务必牢记这一点,因为它通常不仅仅与图像宽度匹配。
现在我们得到了字节,我们需要检查它们,查看它们在当前调色板上的颜色,并在新调色板上找到最接近的匹配。我已经在this answer 中描述了该过程,但为了完整起见,我将在此重复。
将图像与特定颜色匹配的常规方法是在以 R、G 和 B 为轴的 3D 环境中使用颜色之间的毕达哥拉斯距离。请注意,毕达哥拉斯距离计算的“取平方根”部分实际上并不需要;我们不需要知道实际距离,我们只需要比较它们,并且在没有这种相当耗费 CPU 的操作的情况下也能正常工作。
请注意,如果您的图像包含超过 256 个像素(我假设大多数图像都会这样),那么只需查找调色板中每个索引的最接近匹配项,而不是完整图像中的每个像素,就会简单得多,然后将该映射应用于图像数据本身。然后,您只需为每种实际颜色进行一次颜色查找。
/// <summary>
/// Uses Pythagorean distance in 3D colour space to find the closest match to a given colour on
/// a given colour palette, and returns the index on the palette at which that match was found.
/// </summary>
/// <param name="col">The colour to find the closest match to</param>
/// <param name="colorPalette">The palette of available colours to match</param>
/// <returns>The index on the palette of the colour that is the closest to the given colour.</returns>
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;
}
通过为旧调色板中的所有颜色找到最佳颜色匹配,您可以检查图像数据本身,并将图像上的所有索引替换为与新调色板正确匹配的索引。在我们得到所有部分之后,我会将该代码放在最后。
完成后,您就可以准备好将 8 位图像阵列转换回 8 位图像了。这是通过相同的LockBits 操作完成的,只是现在处于“写入”模式。最后,您将新调色板应用于新图像,就像您自己的代码所做的那样。这是我用于所有这些的 BuildImage 函数:
/// <summary>
/// Creates a bitmap based on data, width, height, stride and pixel format.
/// </summary>
/// <param name="sourceData">Byte array of raw source data</param>
/// <param name="width">Width of the image</param>
/// <param name="height">Height of the image</param>
/// <param name="stride">Scanline length inside the data</param>
/// <param name="pixelFormat">Pixel format</param>
/// <param name="palette">Color palette</param>
/// <param name="defaultColor">Default color to fill in on the palette if the given colors don't fully fill it.</param>
/// <returns>The new image</returns>
public static Bitmap BuildImage(Byte[] sourceData, Int32 width, Int32 height, Int32 stride, PixelFormat pixelFormat, Color[] palette, Color? defaultColor)
{
Bitmap newImage = new Bitmap(width, height, pixelFormat);
BitmapData targetData = newImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, newImage.PixelFormat);
// Get the actual minimum data width
Int32 newDataWidth = ((Image.GetPixelFormatSize(pixelFormat) * width) + 7) / 8;
// Cache these to avoid unnecessary getter calls.
Int32 targetStride = targetData.Stride;
Int64 scan0 = targetData.Scan0.ToInt64();
// Copy data per line into the target memory.
for (Int32 y = 0; y < height; ++y)
Marshal.Copy(sourceData, y * stride, new IntPtr(scan0 + y * targetStride), newDataWidth);
newImage.UnlockBits(targetData);
// For indexed images, set the palette.
if ((pixelFormat & PixelFormat.Indexed) != 0 && (palette != null || defaultColor.HasValue))
{
if (palette == null)
palette = new Color[0];
ColorPalette pal = newImage.Palette;
Int32 minLen = Math.Min(pal.Entries.Length, palette.Length);
for (Int32 i = 0; i < minLen; ++i)
pal.Entries[i] = palette[i];
// Fill in remainder with default if needed.
if (pal.Entries.Length > palette.Length && defaultColor.HasValue)
for (Int32 i = palette.Length; i < pal.Entries.Length; ++i)
pal.Entries[i] = defaultColor.Value;
newImage.Palette = pal;
}
return newImage;
}
所以,现在,将所有这些结合起来:
Int32 stride;
Int32 width;
Int32 height;
Color[] curPalette;
// Easier as array. Maybe you should do that right away;
// it's always 256 entries anyway.
Color[] newPalette = ColourPalette.ToArray();
Byte[] imageData;
// This 'using' block is kept small; extract the data and then dispose everything.
using (Bitmap image = new Bitmap(filename))
{
if (image.PixelFormat != PixelFormat.Format8bppIndexed)
return;
width = image.Width;
height = image.Height;
curPalette = image.Palette.Entries;
imageData = GetImageData(image, out stride);
}
// Make remap table to translate from old palette indices to new ones.
Byte[] match = new Byte[curPalette.Length];
for (Int32 i = 0; i < curPalette.Length; ++i)
match[i] = (Byte)GetClosestPaletteIndexMatch(curPalette[i], newPalette);
// Go over the actual pixels in the image data and replace the colours.
Int32 currentLineOffset = 0;
for (Int32 y = 0; y < height; ++y)
{
Int32 offset = currentLineOffset;
for (Int32 x = 0; x < width; ++x)
{
// Replace index with index of the closest match found before for that colour.
imageData[offset] = match[imageData[offset]];
// Increase offset on this line
offset++;
}
// Increase to start of next line
currentLineOffset += stride;
}
using (Bitmap newbm = BuildImage(imageData, width, height, stride, PixelFormat.Format8bppIndexed, newPalette, Color.Black))
{
// Old bitmap is already disposed, so there is no issue saving to the same filename now
newbm.Save(filename, ImageFormat.Bmp);
}