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

问题描述 投票:0回答:1

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

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

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

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

输入:

Input]3

输出:

Output4

对于造成这种情况的原因,我确实有一个想法。当一个像素通过“最近的颜色”函数发送时,我让它输出它的 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# algorithm image-processing colors dithering
1个回答
4
投票

看来,当您将误差转移到

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
}
© www.soinside.com 2019 - 2024. All rights reserved.