使用特定调色板在 C# 中实现 Floyd-Steinberg 抖动

Posted

技术标签:

【中文标题】使用特定调色板在 C# 中实现 Floyd-Steinberg 抖动【英文标题】:Implementing Floyd-Steinberg Dithering in C# with specific Palette 【发布时间】:2021-11-13 07:12:00 【问题描述】:

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

一种颜色距离算法,它遍历每个像素,获取其颜色,然后将其更改为调色板中最接近它的颜色,以使该图像没有调色板中未包含的颜色。 一种抖动算法,它遍历每个像素的颜色,并将原始颜色与在周围像素中选择的新调色板颜色之间的差异扩散。

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

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

输入:

]3

输出:

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

这是我正在使用的相关代码和函数。在这一点上,它是我写的东西和我在图书馆或其他 *** 帖子中找到的东西的混合体。我相信主要的抖动算法和 C3 类基本上是直接从this Github page 复制而来的(显然,改为使用 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();
    

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

【问题讨论】:

您是否尝试过在构造函数中限制传入的值?看来,当您将错误转移到 floydSteinbergDithering() 中的邻居时,r、g、b 值永远不会被钳制,直到您将它们转换回 Color。由于您使用的是 int 而不是 byte,因此无法防止溢出到 rgb 的负值和大正值。这似乎不是一个性能关键的应用程序。您应该考虑将 r、g 和 b 实现为在设置时限制为 0-255 的属性。 另一种可能性是因为这是基于 C# 的 java 代码,可能存在 CLI 差异,其中:Rdiff * Rdiff + Gdiff * Gdiff + Bdiff * Bdiff 的解释与预期顺序不同,请尝试明确定义它,以免产生歧义(Rdiff * Rdiff) + (Gdiff * Gdiff) + (Bdiff * Bdiff) @DekuDesu 谢谢,固定值修复了大部分主要问题!现在有不少图像看起来不错,但有些似乎仍然不能很好地选择颜色,特别是更亮的颜色。这是一个链接,其中包含比较我周围的图像之前和之后的更多内容。我将继续尝试弄乱颜色距离算法,因为这似乎是这里的问题,但抖动似乎现在一切正常。 imgur.com/a/7f2nSSk 【参考方案1】:

看来,当您将错误转移到 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

【讨论】:

这与更准确的颜色差异公式一起使它完美地工作,谢谢!

以上是关于使用特定调色板在 C# 中实现 Floyd-Steinberg 抖动的主要内容,如果未能解决你的问题,请参考以下文章

在控制台应用程序 C# 中实现单例的最佳方法?

在 C# 中实现 runas

在 C# 中实现 IDisposable [重复]

如何使用 C# 在 access 数据库中实现“右外连接”查询?

如何在 C# 中实现线程关联?

使用 Rfc2898DeriveBytes 在 C# 中实现 PBKDF2