首页 > 解决方案 > 使用特定调色板在 C# 中实现 Floyd-Steinberg 抖动

问题描述

我正在制作一个程序,我想在其中拍摄图像并将其调色板减少到 60 种颜色的预设调色板,然后添加抖动效果。这似乎涉及两件事:

在阅读了颜色差异之后,我想我会使用 CIE94 或 CIEDE2000 算法从我的列表中找到最接近的颜色。我还决定使用相当常见的Floyd-Steinberg 抖动算法来实现抖动效果。

在过去的 2 天里,我编写了自己的这些算法版本,从互联网上的示例中提取了它们的其他版本,首先在 Java 和现在的 C# 中尝试了它们,并且几乎每次输出图像都有相同的问题。它的某些部分看起来非常好,具有正确的颜色,并且抖动得当,但是其他部分(有时是整个图像)最终变得太亮,完全是白色的,或者全部模糊在一起。通常较暗的图像或图像的较暗部分会很好,但任何明亮或颜色较浅的部分都会变得更亮。以下是具有这些问题的输入和输出图像的示例:

输入:

输入] 3

输出:

输出4

对于可能导致这种情况的原因,我确实有一个想法。当一个像素通过“最近的颜色”函数发送时,我让它输出它的 RGB 值,似乎其中一些的 R 值(以及可能的其他值??)推得比它们应该的高得多,甚至有时如屏幕截图所示,超过 255 个。这不会发生在图像中最早的像素上,只会发生在已经有多个像素并且已经有点亮的像素上。这让我相信这是抖动/错误算法这样做,而不是颜色转换或色差算法。如果这是问题所在,那么我将如何解决这个问题?

这是我正在使用的相关代码和功能。在这一点上,它是我写的东西和我在图书馆或其他 StackOverflow 帖子中找到的东西的混合体。我相信主要的抖动算法和 C3 类基本上是直接从这个 Github 页面复制的(显然,改为使用 C#)

class Program
{
    public static C3[] palette = new C3[]{
        new C3(196, 76, 86),
        new C3(186, 11, 39),
        new C3(113, 0, 32),
        new C3(120, 41, 56),
        new C3(203, 125, 84),
        new C3(205, 90, 40),
        new C3(175, 50, 33),
        new C3(121, 61, 54),
        // etc... palette is 60 colors total
        // each object contains an r, g, and b value
    };

    static void Main(string[] args)
    {
        // paths for original image and output path for dithered image
        string path = @"C:\Users\BillehBawb\Desktop\";
        string imgPath = path + "amy.jpg";
        string createPath = path + "amydithered.jpg";

        // pulls the original image, runs the dithering function, then saves the new image
        Bitmap img = new Bitmap(imgPath);
        Bitmap dithered = floydSteinbergDithering(img);
        dithered.Save(createPath, ImageFormat.Jpeg);
    }

    // loops through every pixel in the image, populates a 2d array with the pixel colors, then loops through again to change the color to one in the palette and do the dithering algorithm 
    private static Bitmap floydSteinbergDithering(Bitmap img)
    {
        int w = img.Width;
        int h = img.Height;

        C3[,] d = new C3[h, w];

        for (int y = 0; y < h; y++)
        {
            for (int x = 0; x < w; x++)
            {
                d[y, x] = new C3(img.GetPixel(x, y).ToArgb());
            }
        }

        for (int y = 0; y < img.Height; y++)
        {
            for (int x = 0; x < img.Width; x++)
            {

                C3 oldColor = d[y, x];
                C3 newColor = findClosestPaletteColor(oldColor, palette);
                img.SetPixel(x, y, newColor.toColor());

                C3 err = oldColor.sub(newColor);

                if (x + 1 < w)
                {
                    d[y, x + 1] = d[y, x + 1].add(err.mul(7.0 / 16));
                }

                if (x - 1 >= 0 && y + 1 < h)
                {
                    d[y + 1, x - 1] = d[y + 1, x - 1].add(err.mul(3.0 / 16));
                }

                if (y + 1 < h)
                {
                    d[y + 1, x] = d[y + 1, x].add(err.mul(5.0 / 16));
                }

                if (x + 1 < w && y + 1 < h)
                {
                    d[y + 1, x + 1] = d[y + 1, x + 1].add(err.mul(1.0 / 16));
                }
            }
        }

        return img;
    }

    // loops through the palette, converts the input pixel and palette colors to the LAB format, finds the difference between all of them, and selects the palette color with the lowest difference
    private static C3 findClosestPaletteColor(C3 c, C3[] palette)
    {
        double[] pixelLab = rgbToLab(c.toColor().R, c.toColor().G, c.toColor().B);

        double minDist = Double.MaxValue;
        int colorIndex = 0;

        for (int i = 0; i < palette.Length; i++)
        {
            double[] colors = rgbToLab(palette[i].toColor().R, palette[i].toColor().G, palette[i].toColor().B);
            double dist = labDist(pixelLab[0], pixelLab[1], pixelLab[2], colors[0], colors[1], colors[2]);
            if (dist < minDist)
            {
                colorIndex = i;
                minDist = dist;
            }
        }
        return palette[colorIndex];
    }

    // finds the deltaE/difference between two sets of LAB colors with the CIE94 algorithm
    public static double labDist(double l1, double a1, double b1, double l2, double a2, double b2)
    {
        var deltaL = l1 - l2;
        var deltaA = a1 - a2;
        var deltaB = b1 - b2;

        var c1 = Math.Sqrt(Math.Pow(a1, 2) + Math.Pow(b1, 2));
        var c2 = Math.Sqrt(Math.Pow(a2, 2) + Math.Pow(b2, 2));
        var deltaC = c1 - c2;

        var deltaH = Math.Pow(deltaA, 2) + Math.Pow(deltaB, 2) - Math.Pow(deltaC, 2);
        deltaH = deltaH < 0 ? 0 : Math.Sqrt(deltaH);

        double sl = 1.0;
        double kc = 1.0;
        double kh = 1.0;

        double Kl = 1.0;
        double K1 = .045;
        double K2 = .015;

        var sc = 1.0 + K1 * c1;
        var sh = 1.0 + K2 * c1;

        var i = Math.Pow(deltaL / (Kl * sl), 2) +
                Math.Pow(deltaC / (kc * sc), 2) +
                Math.Pow(deltaH / (kh * sh), 2);
        var finalResult = i < 0 ? 0 : Math.Sqrt(i);

        return finalResult;
    }

    // converts RGB colors to the XYZ and then LAB format so the color difference algorithm can be done
    public static double[] rgbToLab(int R, int G, int B)
    {
        float[] xyz = new float[3];
        float[] lab = new float[3];
        float[] rgb = new float[3];

        rgb[0] = R / 255.0f;
        rgb[1] = G / 255.0f;
        rgb[2] = B / 255.0f;

        if (rgb[0] > .04045f)
        {
            rgb[0] = (float)Math.Pow((rgb[0] + .055) / 1.055, 2.4);
        }
        else
        {
            rgb[0] = rgb[0] / 12.92f;
        }

        if (rgb[1] > .04045f)
        {
            rgb[1] = (float)Math.Pow((rgb[1] + .055) / 1.055, 2.4);
        }
        else
        {
            rgb[1] = rgb[1] / 12.92f;
        }

        if (rgb[2] > .04045f)
        {
            rgb[2] = (float)Math.Pow((rgb[2] + .055) / 1.055, 2.4);
        }
        else
        {
            rgb[2] = rgb[2] / 12.92f;
        }
        rgb[0] = rgb[0] * 100.0f;
        rgb[1] = rgb[1] * 100.0f;
        rgb[2] = rgb[2] * 100.0f;


        xyz[0] = ((rgb[0] * .412453f) + (rgb[1] * .357580f) + (rgb[2] * .180423f));
        xyz[1] = ((rgb[0] * .212671f) + (rgb[1] * .715160f) + (rgb[2] * .072169f));
        xyz[2] = ((rgb[0] * .019334f) + (rgb[1] * .119193f) + (rgb[2] * .950227f));


        xyz[0] = xyz[0] / 95.047f;
        xyz[1] = xyz[1] / 100.0f;
        xyz[2] = xyz[2] / 108.883f;

        if (xyz[0] > .008856f)
        {
            xyz[0] = (float)Math.Pow(xyz[0], (1.0 / 3.0));
        }
        else
        {
            xyz[0] = (xyz[0] * 7.787f) + (16.0f / 116.0f);
        }

        if (xyz[1] > .008856f)
        {
            xyz[1] = (float)Math.Pow(xyz[1], 1.0 / 3.0);
        }
        else
        {
            xyz[1] = (xyz[1] * 7.787f) + (16.0f / 116.0f);
        }

        if (xyz[2] > .008856f)
        {
            xyz[2] = (float)Math.Pow(xyz[2], 1.0 / 3.0);
        }
        else
        {
            xyz[2] = (xyz[2] * 7.787f) + (16.0f / 116.0f);
        }

        lab[0] = (116.0f * xyz[1]) - 16.0f;
        lab[1] = 500.0f * (xyz[0] - xyz[1]);
        lab[2] = 200.0f * (xyz[1] - xyz[2]);

        return new double[] { lab[0], lab[1], lab[2] };
    }
}

这是 C3 类,它基本上只是带有一些数学函数的 Color 类,以使其更清晰地进行抖动

class C3
{
    int r, g, b;

    public C3(int c)
    {
        Color color = Color.FromArgb(c);
        r = color.R;
        g = color.G;
        b = color.B;
    }

    public C3(int r, int g, int b)
    {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    public C3 add(C3 o)
    {
        return new C3(r + o.r, g + o.g, b + o.b);
    }

    public int clamp(int c)
    {
        return Math.Max(0, Math.Min(255, c));
    }

    public int diff(C3 o)
    {
        int Rdiff = o.r - r;
        int Gdiff = o.g - g;
        int Bdiff = o.b - b;
        int distanceSquared = Rdiff * Rdiff + Gdiff * Gdiff + Bdiff * Bdiff;
        return distanceSquared;
    }

    public C3 mul(double d)
    {
        return new C3((int)(d * r), (int)(d * g), (int)(d * b));
    }

    public C3 sub(C3 o)
    {
        return new C3(r - o.r, g - o.g, b - o.b);
    }

    public Color toColor()
    {
        return Color.FromArgb(clamp(r), clamp(g), clamp(b));
    }

    public int toRGB()
    {
        return toColor().ToArgb();
    }
}

对不起,大量的代码转储,功能非常大,我想提供我能做的一切。如果有人对更简单或不同的方法有任何建议,或者如果您知道如何解决我遇到的问题,请告诉我。我已经尝试了很多不同的算法来获得我想要的结果,但是我无法让它们中的任何一个来做我想要他们做的事情。非常感谢任何帮助或想法,谢谢!

标签: c#algorithmimageimage-processingcolors

解决方案


看来,当您将错误转移到floydSteinbergDithering()r,g,b 中的邻居时,值永远不会被钳制,直到您将它们重新转换为Color.

由于您使用的是 int 而不是 byte,因此无法防止 r、g 和 b 溢出到大于 255 的负值或大值。

您应该考虑将 r、g 和 b 实现为在设置时钳制为 0-255 的属性。

这将确保它们的值永远不会超出您的预期范围(0 - 255)。

class C3
{
    private int r;
    public int R
    {
        get => r;
        set
        {
            r = clamp(value);
        }
     }

    private int g;
    public int G
    {
        get => g;
        set
        {
            g = clamp(value);
        }
     }

    private int b;
    public int B
    {
        get => b;
        set
        {
            b = clamp(value);
        }
     }

    // rest of class
}

推荐阅读