午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

減色テスト、グレースケール版その4、誤差拡散すると誤差蓄積されすぎて色が流れたようになる問題

誤差拡散法 誤差蓄積
f:id:gogowaten:20200418114533p:plain
誤差拡散法を使って減色できるようにした、方式はフロイドスタインバーグ

20200327_減色テストグレースケールvar.1.3.zip

github.com



誤差蓄積されすぎ問題
f:id:gogowaten:20200418122032p:plain
この画像を2色に減色する、減色用のパレットを作成
f:id:gogowaten:20200418122128p:plain
2色は161と97が選ばれた、誤差拡散して減色すると
f:id:gogowaten:20200418121846p:plain
普通に処理したのが右上の画像、色が右下に流れたような画像になってしまった

減色用のパレットに選ばれる色は、少ない色で多くの色をカバーすることになるから、どうしても中間的な色になる
元の画像の色は0~255
パレットの色が2色で192と64だと、0~127は64に変換、128~255は192に変換することになる
元の画像に255が続いたあとに120が続く場所がある場合を見ると
f:id:gogowaten:20200418124637p:plain
255と192の差は63なので、255が続くと63づつ誤差が蓄積される
インデックス3のところまでの誤差は444と大きくなっているので、そのあと120が続いてもインデックス7までは192に変換されてしまう、これが色が流れたようになる原因
誤差拡散法は誤差を右と下方向へ拡散するので右下に流れたようになる

誤差を制限
拡散した先の値を0~255に収める、もと+誤差が0以下になったら0にして、255以上なら255にする
誤差拡散法なのに拡散しないで切り捨てるのは矛盾しているけど、他に思いつかない
f:id:gogowaten:20200418130618p:plain
さっきより良くなった
色だけ並べてみると
f:id:gogowaten:20200418130819p:plain
いいね、元の色が120のところに64が増えて元の画像に近づいた

実際の画像処理で比較
f:id:gogowaten:20200418131721p:plain
まだ少し流れているけど、かなり良くなった

更に制限して、パレットの最小値と最大値を誤差蓄積の下限上限にする
f:id:gogowaten:20200418132644p:plain
f:id:gogowaten:20200418132904p:plain

f:id:gogowaten:20200418133101p:plain
あんまり変わっていないけど、並べると判るくらいには良くなった

制限がなくてもパレットの色に0と255があれば流れない
f:id:gogowaten:20200418133746p:plain
0と255の2色だと制限なしでも色が流れることなく期待通りの画像になる

f:id:gogowaten:20200418134019p:plain
0,85,170,255の4色、これも0と255があるから色が流れない

0と255がなくても色数が多くなると、制限なしでも色流れが気にならない
f:id:gogowaten:20200418134724p:plain
4色だと流れているのが判るけど、16色だと全く気にならない
4, 8, 16色で比較

まとめて比較
f:id:gogowaten:20200418140140p:plain
0と255があるパレットだと制限の有無の違いはほとんどない、4色の方は全くおなじに見えるくらい
画像にも依るけど、制限ありだけで見ても0と255があるパレットのほうが元の画像に近い感じがする。でも、今の減色パレット作成処理だと、分割Cubeからの色の選択は、ピクセル平均値、中央値、辺中央だから中間的な色になるから、仮に0や255が含まれるCubeがあっても選択されることはないからなあ




/// <summary>
/// Floyd_Steinberg_dithering、8bitグレースケール専用、パレットの最小値と最大値で誤差蓄積の制限して誤差拡散処理
/// </summary>
/// <param name="palette">輝度</param>
/// <param name="source">画素</param>
/// <param name="width">画像の横ピクセル数</param>
/// <param name="height">画像の縦ピクセル数</param>
/// <param name="stride">画像の1行分のbyte数(1ピクセルあたりのbyte * 横ピクセル数)</param>
/// <returns></returns>
public static byte[] Gensyoku誤差拡散2(byte[] palette, byte[] source, int width, int height, int stride)
{
    //パレットの最小値と最大値で誤差蓄積の制限をする
    byte lower = palette.Min();
    byte upper = palette.Max();

    int count = source.Length;
    byte[] pixels = new byte[count];//変換先画像用
    double[] gosaPixels = new double[count];//誤差計算用
    Array.Copy(source, gosaPixels, count);
    int p;//座標
    double gosa;//誤差(変換前 - 変換後)

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            p = y * stride + x;
            //パレットから一番近い色に置き換える
            double min = double.MaxValue;
            double distance;
            byte color = palette[0];
            for (int i = 0; i < palette.Length; i++)
            {
                distance = Math.Abs(gosaPixels[p] - palette[i]);
                if (distance < min)
                {
                    min = distance;
                    color = palette[i];
                }
            }
            pixels[p] = color;//置き換え

            //誤差拡散
            gosa = gosaPixels[p] - color;
            gosa /= 16.0;
            if (x < width - 1)
                SetLimitedGosa(gosaPixels, p + 1, gosa * 7, lower, upper);

            if (y < height - 1)
            {
                p += stride;
                SetLimitedGosa(gosaPixels, p, gosa * 5, lower, upper);

                if (x > 0)
                    SetLimitedGosa(gosaPixels, p - 1, gosa * 3, lower, upper);

                if (x < width - 1)
                    SetLimitedGosa(gosaPixels, p + 1, gosa * 1, lower, upper);
            }
        }
    }
    return pixels;
}

/// <summary>
/// 下限上限を設定して誤差を足す、下限上限を超えたら切り捨て
/// </summary>
/// <param name="gosaPixels">誤差を足した値を入れる配列</param>
/// <param name="p">配列のインデックス</param>
/// <param name="gosa">足す誤差</param>
/// <param name="lower">下限</param>
/// <param name="upper">上限</param>
private static void SetLimitedGosa(double[] gosaPixels, int p, double gosa, byte lower, byte upper)
{
    double result = gosaPixels[p] + gosa;
    if (result < lower)
        result = lower;
    if (result > upper)
        result = upper;
    gosaPixels[p] = result;
}

これをBitmapSourceとパレットの色リストListを渡して使うときは

public static BitmapSource Gensyoku誤差拡散(BitmapSource bitmap, byte[] palette)
{
    int w = bitmap.PixelWidth;
    int h = bitmap.PixelHeight;
    int stride = w;// (bitmap.Format.BitsPerPixel + 7) / 8;
    byte[] sourcePixels = new byte[h * stride];
    bitmap.CopyPixels(sourcePixels, stride, 0);
    byte[] pixels = Gensyoku誤差拡散2(palette, sourcePixels, w, h, stride);
    return BitmapSource.Create(w, h, 96, 96, PixelFormats.Gray8, null, pixels, stride);
}
public static BitmapSource Gensyoku誤差拡散(BitmapSource bitmap, List<Color> palette)
{
    byte[] vs = palette.Select(x => x.R).ToArray();
    return Gensyoku誤差拡散(bitmap, vs);
}

これもBitmapSourceのピクセルフォーマットはGray8専用
Gray8以外のBitmapSourceならFormatConvertedBitmapクラスでGray8に変換して使う

減色処理だけをするクラス
f:id:gogowaten:20200418150020p:plain
Gensyoku.csって名前にして別のファイルにしてみたのと、フィールドがあるわけでもなく、計算して結果を返すだけのクラスをnewするのもめんどくさいので、staticなメソッドにしてみた
staticなのはあんまり書いたことなくて、newしないで使えるからラクじゃんくらいしか思っていないけど、どうなのかなあ、問題なさそうならこのまま

f:id:gogowaten:20200418152139p:plain
f:id:gogowaten:20200418153149p:plain
f:id:gogowaten:20200418153654p:plain
グレースケール飽きた

関連記事
次回のWPF記事は

gogowaten.hatenablog.com

前回は昨日
gogowaten.hatenablog.com

前回の誤差拡散記事は2週間前
gogowaten.hatenablog.com