c# - 如何将彩色图像转换为仅具有两种预定义颜色的图像?

标签 c# image image-processing

我正在尝试将彩色图像转换为只有两种颜色的图像。我的方法是先使用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;
}

最佳答案

将图像与特定颜色匹配的正常方法是在以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属性实际上创建了一个新的ColorPalette对象,并且未引用图像中的那个对象,因此代码image.Palette.Entries[0] = Color.Blue;将不起作用,因为它只会修改该未引用的副本。因此,必须始终将调色板对象取出,编辑然后重新分配给图像。

如果您需要将结果保存到相同的文件名there's a trick with a stream you can use,但是如果您只需要将对象的调色板更改为这两种颜色,就可以了。



如果您不确定原始图像格式,则过程会涉及很多:

如前面的注释中所述,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;

关于c# - 如何将彩色图像转换为仅具有两种预定义颜色的图像?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45468798/

相关文章:

html - 对齐下方和彼此相邻的图像

image-processing - 如何识别图片中我们感兴趣的物体类型

c# - 在这种情况下我真的应该实现 IDisposable 吗?

c# - 当方法信息仅在运行时已知时优化 MethodInfo.Invoke

c# - Asp.NET 4.0 GridView 默认排序方向和表达式

c# - 如果不存在则更改表添加 Cassandra

javascript - 如何在加载图像时立即运行一些代码?

css - 多图网页正文背景

c# - 为什么在 WPF 中将 BitmapSource 保存为 bmp、jpeg 和 png 时得到完全不同的结果

android - Android 的图像效果?