如何将彩色图像转换为只有两种预定义颜色的图像? [英] How to convert a colored image to a image that has only two predefined colors?

查看:150
本文介绍了如何将彩色图像转换为只有两种预定义颜色的图像?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am trying to convert a colored image to a image that only has two colors. My approach was first converting the image to a black and white image by using Aforge.Net Threshold class and then convert the black and white pixels into colors that I want. The display is on real-time so this approach introduces a significant delay. I was wondering if there's a more straightforward way of doing this.

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环境中颜色之间的毕达哥拉斯距离。我有一堆用于处理图像和颜色的工具集,我对任何外部框架都不太熟悉,所以我只是挖掘我的东西并给你相关的功能。

The normal way to match an image to specific colours is to use Pythagorean distance between the colours in a 3D environment with R, G and B as axes. I got a bunch of toolsets for manipulating images and colours, and I'm not too familiar with any external frameworks, so I'll just dig through my stuff and give you the relevant functions.

首先,颜色替换本身。此代码将匹配您在有限调色板上为最接近的可用颜色提供的任何颜色,并返回给定数组中的索引。请注意,我遗漏了毕达哥拉斯距离计算的取平方根部分;我们不需要知道实际的距离,我们只需要比较它们,并且在没有相当CPU操作的情况下也能正常工作。

First of all, the colour replacement itself. This code will match any colour you give to the closest available colour on a limited palette, and return the index in the given array. Note that I left out the "take the square root" part of the Pythagorean distance calculation; we don't need to know the actual distance, we only need to compare them, and that works just as well without that rather CPU-heavy operation.

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个:

Now, on a high-coloured image, this palette matching would have to be done for every pixel on the image, but if your input is guaranteed to be paletted already, then you can just do it on the colour palette, reducing your palette lookups to just 256 per image:

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], palette);
    pal.Entries[i] = palette[foundIndex];
}
image.Palette = pal;

就是这样;调色板上的所有颜色都替换为最接近的匹配。

And that's it; all colours on the palette replaced by their closest match.

请注意,调色板属性实际上会生成新的 ColorPalette对象,并没有引用图像中的那个,所以代码 image.Palette.Entries [0] = Color.Blue; 不会工作,因为它只是修改那个未引用的副本。因此,调色板对象总是必须被取出,编辑然后重新分配给图像。

Note that the Palette property actually makes a new ColorPalette object, and doesn't reference the one in the image, so the code image.Palette.Entries[0] = Color.Blue; would not work, since it'd just modify that unreferenced copy. Because of that, the palette object always has to be taken out, edited and then reassigned to the image.

如果需要将结果保存到相同的文件名,< a href =https://stackoverflow.com/a/48579791/395685>有一个你可以使用的流技巧,但是如果你只需要将对象的调色板改为这两种颜色,这就是它。

If you need to save the result to the same filename, there's a trick with a stream you can use, but if you simply need the object to have its palette changed to these two colours, that's really it.

如果你确定原始图像格式,过程涉及到更多:

In case you are not sure of the original image format, the process is quite a bit more involved:

如前所述, GetPixel SetPixel 非常慢,访问图像的底层字节效率更高。但是,除非您100%确定输入类型的像素格式是什么,否则您不能只访问这些字节,因为您需要知道如何读取它们。一个简单的解决方法就是让框架为您完成工作,方法是在每像素新的32位图像上绘制现有图像:

As mentioned before in the comments, GetPixel and SetPixel are extremely slow, and it's much more efficient to access the image's underlying bytes. However, unless you are 100% certain what your input type's pixel format is, you can't just go and access these bytes, since you need to know how to read them. A simple workaround for this is to just let the framework do the work for you, by painting your existing image on a new 32 bits per pixel image:

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 ,以提供一个背景来从源图像中删除任何透明度。

Now, you probably want to make sure transparent pixels don't end up as whatever colour happens to be hiding behind an alpha value of 0, so you better specify the transparencyFillColor in this function to give a backdrop to remove any transparency from the source image.

现在我们得到了高彩色图像,下一步是越过图像字节,将它们转换为ARGB颜色,并使用我给出的函数将它们与调色板匹配之前。我建议制作一个8位图像,因为它们最容易编辑为字节,事实上它们有一个调色板,这使得在创建它们之后更换颜色非常容易。

Now we got the high-colour image, the next step is going over the image bytes, converting them to ARGB colours, and matching those to the palette, using the function I gave before. I'd advise making an 8-bit image because they're the easiest to edit as bytes, and the fact they have a colour palette makes it ridiculously easy to replace colours on them after they're created.

无论如何,字节。大文件可以更有效地立即迭代不安全内存中的字节,但我通常更喜欢将它们复制出来。你选择的当然;如果您认为值得,可以将以下两个功能组合起来直接访问它。 这是直接访问颜色字节的一个很好的例子

Anyway, the bytes. It's probably more efficient for large files to iterate through the bytes in unsafe memory right away, but I generally prefer copying them out. Your choice, of course; if you think it's worth it, you can combine the two functions below to access it directly. 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-位图,迭代每四个字节,并匹配调色板中的颜色。请注意,您可以从不假设一行像素的实际字节长度(步幅)等于宽度乘以每像素的字节数。因此,虽然代码只是将像素大小添加到读取偏移量以获得一行上的下一个像素,但它使用步幅来跳过数据中的整行像素。

Now, all you need to do is make an array to represent your 8-bit image, iterate over all bytes per four, and match the colours you get to the ones in your palette. Note that you can never assume that the actual byte length of one line of pixels (the stride) equals the width multiplied by the bytes per pixel. Because of this, while the code does simply add the pixel size to the read offset to get the next pixel on one line, it uses the stride for skipping over whole lines of pixels in the data.

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位数组。要将该数组转换为图像,您可以使用我已发布的 BuildImage 函数另一个答案

Now we got our 8-bit array. To convert that array to an image you can use the BuildImage function I already posted on another answer.

最后,使用这些工具,转换代码应该是这样的:

So finally, using these tools, the conversion code should be something like this:

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位调色板图像。

There we go; your image is converted to 8-bit paletted image, for whatever palette you want.

如果您想实际匹配黑白和那么更换颜色,这也没问题;只使用仅包含黑色和白色的调色板进行转换,然后获取生成的位图的Palette对象,替换其中的颜色,并将其分配回图像。

If you want to actually match to black and white and then replace the colours, that's no problem either; just do the conversion with a palette containing only black and white, then take the resulting bitmap's Palette object, replace the colours in it, and assign it back to the image.

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;

这篇关于如何将彩色图像转换为只有两种预定义颜色的图像?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆