【问题标题】:How to convert a colored image to a image that has only two predefined colors?如何将彩色图像转换为只有两种预定义颜色的图像?
【发布时间】:2018-01-10 03:34:38
【问题描述】:

我正在尝试将彩色图像转换为只有两种颜色的图像。我的方法是首先使用 Aforge.Net Threshold 类将图像转换为黑白图像,然后将黑白像素转换为我想要的颜色。显示是实时的,因此这种方法引入了显着的延迟。我想知道是否有更直接的方法可以做到这一点。

Bitmap image = (Bitmap)eventArgs.Frame.Clone();
Grayscale greyscale = new Grayscale(0.2125, 0.7154, 0.0721);
Bitmap grayImage = greyscale.Apply(image);
Threshold threshold = new Threshold(trigger);
threshold.ApplyInPlace(grayImage);
Bitmap colorImage = CreateNonIndexedImage(grayImage);
if (colorFilter)
{
    for (int y = 0; y < colorImage.Height; y++)
    {
        for (int x = 0; x < colorImage.Width; x++)
        {

            if (colorImage.GetPixel(x, y).R == 0 && colorImage.GetPixel(x, y).G == 0 && colorImage.GetPixel(x, y).B == 0)
            {
                colorImage.SetPixel(x, y, Color.Blue);
            }
            else
            {
                colorImage.SetPixel(x, y, Color.Yellow);
            }
        }
    }
}

private Bitmap CreateNonIndexedImage(Image src)
{
    Bitmap newBmp = new Bitmap(src.Width, src.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    using (Graphics gfx = Graphics.FromImage(newBmp))
    {
        gfx.DrawImage(src, 0, 0);
    }

    return newBmp;
}

【问题讨论】:

  • 您是否对各个步骤进行了计时,以确定处理的哪个阶段较慢?我的猜测是你的 for 循环是性能猪,但这只是一个猜测。另外,为什么不直接定义一个颜色图,并为颜色图使用 Image/Bitmap 构造函数,而不是自己定义颜色图?
  • GetPixelSetPixel 非常慢,不要使用它们。
  • 你到底想做什么?您是否真的想要重新着色的黑白转换,或者您想要尽可能地匹配图像与黄色和蓝色的颜色?因为这些动作根本不一样。对于最接近的颜色匹配,一般的方法是在以 R、G 和 B 为轴的 3D 环境中计算颜色之间的毕达哥拉斯距离。
  • 顺便说一句,CreateNonIndexedImage 是否意味着您的原始黑白转换图像已编入索引?因为这样您就可以简单地将索引图像的调色板颜色更改为黄色和蓝色,然后您就完成了。虽然据我所见,它看起来会创建一个 256 色调的灰度图像,而不是纯黑/白图像。
  • @Nyerguds 是的,原始图像已编入索引。您能否给我一些关于如何更改索引图像的调色板颜色的指示?

标签: c# image image-processing


【解决方案1】:

将图像与特定颜色匹配的常规方法是在以 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 中提到的,GetPixelSetPixel 非常慢,访问图像的底层字节效率更高。但是,除非您 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;

【讨论】:

  • 对此的小补充...我后来发现 LockBits 可以将其输出数据转换为某种像素格式。所以如果你只给PixelFormat.Format32BppArgb而不是sourceImage.PixelFormat,整个PaintOn32bpp步骤是不必要的,除非可能是为了摆脱透明度。
猜你喜欢
  • 2013-05-09
  • 1970-01-01
  • 2011-07-10
  • 1970-01-01
  • 2015-05-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-25
相关资源
最近更新 更多