Применение палитры — это не то же самое, что замена палитры. Шаг, который вам не хватает, — это сопоставить фактические пиксели изображения с новыми цветами.
Видите, пиксели на проиндексированных изображениях не содержат цветов. Они содержат ссылки на палитру. Итак, если у вас есть красный пиксель на изображении, это не пиксель со значением красного, это пиксель со значением от 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 байтам, жизненно важно помнить об этом, потому что очень часто это не будет соответствовать ширине изображения.
Теперь мы получили байты, нам нужно пройтись по ним, посмотреть, какой у них цвет на текущей палитре, и найти наиболее близкое совпадение на новой палитре. Я уже описывал этот процесс в этом ответе, но я повторю его здесь для полноты картины.
Обычный способ сопоставить изображение с определенными цветами — использовать пифагорейское расстояние между цветами в трехмерной среде с осями R, G и B. Обратите внимание, что вычисление расстояния по Пифагору на самом деле не требуется; нам не нужно знать фактическое расстояние, нам нужно только сравнить их, и это работает так же хорошо без этой довольно тяжелой операции с процессором.
Обратите внимание, что если ваше изображение содержит более 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);
}
16.07.2021