【问题标题】:How to correctly apply color palette to an image?如何正确地将调色板应用于图像?
【发布时间】:2021-08-15 14:22:16
【问题描述】:

因此,我被赋予了重新编写一个程序的任务,该程序将图像转换为 3 种不同的尺寸,其中 1 是应用了特定调色板的 256 色图像。原始源代码丢失了。

调整大小我开始工作,但我在应用调色板时遇到问题。 调色板存储为 JASC-PAL 文件。

这是我的代码,假设您只是加载文件、循环图像的当前调色板并替换颜色。

private List<Color> ColourPalette = new List<Color>();
void LoadColourPalette()
    {
        using (StreamReader sr = new StreamReader("HH.PAL"))
        {
            // skip first 3 lines
            sr.ReadLine();
            sr.ReadLine();
            sr.ReadLine();

            while (sr.Peek() != -1)
            {
                var readLine = sr.ReadLine();

                if(string.IsNullOrWhiteSpace(readLine))
                    continue;

                var colourBytes = readLine.Split(' ');

                ColourPalette.Add(
                    Color.FromArgb(int.Parse(colourBytes[0]),
                        int.Parse(colourBytes[1]),
                        int.Parse(colourBytes[2])
                        ));

            }
        }
    }

加载文件并应用调色板。

byte[] bytes;

using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
    using (var thumbnail = System.Drawing.Image.FromStream(fs))
    {
       var imagePalette = thumbnail.Palette;

       for (int i = 0; i < imagePalette.Entries.Length; i++)
           imagePalette.Entries[i] = ColourPalette[i];

       thumbnail.Palette = imagePalette;

       using (MemoryStream memory = new MemoryStream())
       {
           thumbnail.Save(memory, ImageFormat.Bmp);
           bytes = memory.ToArray();
       }
    }
}

using (FileStream fs = new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite))
{
   fs.Write(bytes, 0, bytes.Length);
}

当我查看图像时,它看起来完全错误。看起来更像是有人扔掉了像素化的油漆哈哈。 我无法上传图片,但为了让您了解,图片是放在桌子上的一个白盘子。桌子应该是黑色/深蓝色的阴影,但它们是浅棕色和一些绿色的阴影。

然后我拍摄了一张已应用调色板的图像,我提取了调色板并保存到 JASC-PAL 格式的文件中。当我与我提供的调色板文件进行比较时,我可以看到它们完美匹配。

显然还有更多内容,但我似乎找不到任何关于此事的内容。

【问题讨论】:

  • 没有必要使用FileStreamBitmap 类(Image 的实际实现)有一个 new Bitmap(path) 构造函数,它在内部自动打开一个流,对于写入字节,您可以使用 File.WriteAllBytes(path, byteArray)

标签: c# bitmap color-palette


【解决方案1】:

我会假设您的源图像是 24 位或 32 位颜色。 IE。每个像素对于红绿和蓝通道中的每一个都有一个字节。调色板图像不能这样工作。相反,它只有一个字节,用作查找表的 in 索引。

因此,要找到正确的索引,您需要在调色板中进行搜索,以找到与实际像素颜色最相似的颜色。我不认为您可以使用 8 位位图作为 Graphics.FromImage 的渲染目标,因此您可能需要自己进行此处理,但可能有可用的库可以为您完成。

使用调色位图已不再常见,因为通过使用现代图像压缩算法通常可以更有效地减少任何尺寸。

老实说,如果您只设置常规 24 位位图的调色板,我真的不确定会发生什么,但显然结果不正确,因此可能不相关。

【讨论】:

  • 不,尝试更改 24 位或 32 位图像上的调色板对这段代码没有任何作用,因为调色板长度为 0。它搞砸的事实证明他的源图像已经是 8位。
【解决方案2】:

应用调色板与替换调色板不同。您缺少的步骤是将图像上的实际像素与新颜色相匹配。

请看,索引图像上的像素不包含颜色。它们包含对调色板的引用。因此,如果您在图像上有一个红色像素,那不是一个值为“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);
}

【讨论】:

    猜你喜欢
    • 2019-07-25
    • 2022-01-21
    • 2014-12-15
    • 1970-01-01
    • 2019-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-19
    相关资源
    最近更新 更多