午後わてんのブログ

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

FloydSteinberg他いくつかの誤差拡散を試してみた、白黒2値をディザリング

 
昨日の続き
いくつかの誤差拡散法を試してみた
 
アプリダウンロード先
ここの20180222_.2.zipがそれ
 
イメージ 6
左から順に
元のグレースケール画像
右隣だけに拡散(昨日の)
FloydSteinberg式を小数点以下切り捨て
FloydSteinberg式を普通に
SierraLite
JuJuNi
Atkinson
拡散する値
右隣だけに拡散
イメージ 1
色付きが処理中のピクセル、数値は誤差の分母
右のピクセルに誤差の1/1(全部)を拡散
 
FloydSteinberg
イメージ 2
右隣へは誤差の7/16を拡散
左下、真下、右下へはそれぞれ
3/16, 5/16, 1/16を拡散
 
右隣への誤差拡散法と違うのは誤差に小数点が発生するから、誤差を入れる変数にfloat型を使わないと微妙に間違った結果になってしまうこと
最初気づかなくてなんでお手本と違うのかわかんなかった
あと、白黒判定の閾値も128じゃなくて127.5を使う
 

 
8bitグレースケールのBitmapSourceを白黒2値に変換 
/// <summary>
/// 誤差拡散法、フロイドステインバーグ式でディザリング
/// 8bitグレースケールのBitmapSourceを白黒2値に変換
/// </summary>
/// <param name="source">PixelFormatGray8のBitmapSource</param>
/// <returns></returns>
private BitmapSource ErrorDiffusionFloydSteinberg(BitmapSource source)
{
    var wb = new WriteableBitmap(source);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    var pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);//byte配列にCopyPixel
    //float配列作成してコピー
    float[] iPixels = new float[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }
    long p = 0;//判定中ピクセルの配列の中での位置
    float gosa = 0;//誤差記録用もfloat
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + x;
            if (iPixels[p] < 127.5f)//127.5未満なら0(黒)
            {
                gosa = iPixels[p];//誤差記録
                iPixels[p] = 0;
            }
            else//127.5以上なら255(白)
            {
                gosa = (iPixels[p] - 255f);//誤差記録
                iPixels[p] = 255;
            }

            //誤差拡散、Floyd Steinberg式
            if (p + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + 1] += (gosa / 16f) * 7f;//右隣+誤差(誤差拡散)                 
            }
            if (y < h - 1 && x != 0)
            {
                iPixels[p + stride - 1] += (gosa / 16f) * 3f;//左下
            }
            if (p + stride < pixels.Length)
            {
                iPixels[p + stride] += (gosa / 16f) * 5f;//真下
            }
            if (p + stride + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + stride + 1] += (gosa / 16f) * 1f;//右下
            }
            gosa = 0;//拡散させたので誤差をリセット
        }
    }
    //byte配列に戻す
    for (int i = 0; i < pixels.Length; ++i)
    {
        pixels[i] = (byte)(iPixels[i]);
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}

 

 

 その他の誤差拡散法
SierraLite
イメージ 3
FloydSteinbergよりラク
 
Jarvis, Judice and Ninke(JaJuNi)
イメージ 4
これも代表的な誤差拡散ってことで試したけど
めんどくさいいい
 
Atkinson
イメージ 5
誤差の6/8だけ拡散して残りの2/8は捨て去る大胆な方法
 
イメージ 7
FloydSteinbergとSierraLiteが似ているのは拡散の方法からなんとなくわかるけど
JaJuNiとAtkinsonが似ているなあ、JaJuNiはあんなに処理の手数が多いのに…
それに2/8も誤差を捨て去っているAtkinsonの再現度が高いのが不思議
 
 
 
イメージ 8
RGB(254,254,254)
ほぼ真っ白な灰色の場合
 
イメージ 9
Atkinsonで消え去るのはわかるけど
JaJuNiでも消え去ったのは意外
印象では真っ白な画像なんだからきれいに消え去って真っ白になるのは
ある意味正解だよなあ
イメージ 10
わずかなグラデーションと
普通のグラデーション
 
イメージ 11
こはれJaJuNiとAtkinsonがいいねえ
 
 
ダビデさん
イメージ 12
ディザ - Wikipedia
https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%A3%E3%82%B6
より引用
イメージ 13
それぞれ良さがある
安定のFloydSteinberg
処理の軽さのSierraLite
余計な縞模様が出にくいJaJuNi
印象派のAtkinson
 
 
 
処理の順番(方向)での違い
イメージ 14
元の画像
イメージ 15
左が普通の処理順番 右が蛇行
左画像には斜めの縞模様が出ている、上部に特に目立つものと全体にもでている
右画像は上部に規則的な模様が出ているけど、全体では均等にバラけていて良好
 
普通は
→1行下の左端へ
1行下の左端へ
これの繰り返し
これを
→↓
↓←
→↓
蛇行する
イメージ 16
左へ処理をすすめるときは誤差拡散の方向も逆にしたらうまくできた
 
 
//FloydSteinberg
//処理の順番を蛇行、偶数行は右へ、奇数行は左へ
private BitmapSource ErrorDiffusionFloydSteinbergSerpentine(BitmapSource source)
{
    var wb = new WriteableBitmap(source);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    var pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);//byte配列にCopyPixel
    //float配列作成してコピー
    float[] iPixels = new float[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }
    long p = 0;//判定中ピクセルの配列の中での位置
    float gosa = 0;//誤差記録用もfloat
    for (int y = 0; y < h; ++y)
    {
        if (y % 2 == 0)//偶数行
        {
            for (int x = 0; x < w; ++x)
            {
                p = y * stride + x;
                if (iPixels[p] < 127.5f)//127.5未満なら0(黒)
                {
                    gosa = iPixels[p];//誤差記録
                    iPixels[p] = 0;
                }
                else//127.5以上なら255(白)
                {
                    gosa = (iPixels[p] - 255f);//誤差記録
                    iPixels[p] = 255;
                }

                if (p + 1 < pixels.Length && x < w - 1)
                {
                    iPixels[p + 1] += (gosa / 16f) * 7f;//右隣+誤差(誤差拡散)             
                }
                if (y < h - 1 && x != 0)
                {
                    iPixels[p + stride - 1] += (gosa / 16f) * 3f;//左下
                }
                if (p + stride < pixels.Length)
                {
                    iPixels[p + stride] += (gosa / 16f) * 5f;//真下
                }
                if (p + stride + 1 < pixels.Length && x < w - 1)
                {
                    iPixels[p + stride + 1] += (gosa / 16f) * 1f;//右下
                }
                gosa = 0;//拡散させたので誤差をリセット
            }
        }
        else//奇数行
        {
            for (int x = w - 1; x >= 0; --x)
            {
                p = y * stride + x;
                if (iPixels[p] < 127.5f)//127.5未満なら0(黒)
                {
                    gosa = iPixels[p];//誤差記録
                    iPixels[p] = 0;
                }
                else//127.5以上なら255(白)
                {
                    gosa = (iPixels[p] - 255f);//誤差記録
                    iPixels[p] = 255;
                }

                if (p - 1 < pixels.Length && x != 0)//左隣
                {
                    iPixels[p - 1] += (gosa / 16f) * 7f;
                }
                if (y < h - 1 && x != 0)
                {
                    iPixels[p + stride - 1] += (gosa / 16f) * 1f;//左下
                }
                if (p + stride < pixels.Length)
                {
                    iPixels[p + stride] += (gosa / 16f) * 5f;//真下
                }
                if (p + stride + 1 < pixels.Length && x < w - 1)
                {
                    iPixels[p + stride + 1] += (gosa / 16f) * 3f;//右下
                }
                gosa = 0;//拡散させたので誤差をリセット
            }
        }
    }
    //byte配列に戻す
    for (int i = 0; i < pixels.Length; ++i)
    {
        pixels[i] = (byte)(iPixels[i]);
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}

//SierraLite
private BitmapSource ErrorDiffusionSierraLite(BitmapSource source)
{
    var wb = new WriteableBitmap(source);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    var pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);//byte配列にCopyPixel
    //float配列作成してコピー
    float[] iPixels = new float[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }
    long p = 0;//判定中ピクセルの配列の中での位置
    float gosa = 0;//誤差記録用もfloat
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + x;
            if (iPixels[p] < 127.5f)//127.5未満なら0(黒)
            {
                gosa = iPixels[p];//誤差記録
                iPixels[p] = 0;
            }
            else//127.5以上なら255(白)
            {
                gosa = (iPixels[p] - 255f);//誤差記録
                iPixels[p] = 255;
            }

            //誤差拡散
            if (p + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + 1] += (gosa / 4f) * 2f;//右隣+誤差(誤差拡散)                  
            }
            if (y < h - 1 && x != 0)
            {
                iPixels[p + stride - 1] += (gosa / 4f) * 1f;//左下
            }
            if (p + stride < pixels.Length)
            {
                iPixels[p + stride] += (gosa / 4f) * 1f;//真下
            }
            gosa = 0;//拡散させたので誤差をリセット
        }
    }
    //byte配列に戻す
    for (int i = 0; i < pixels.Length; ++i)
    {
        pixels[i] = (byte)(iPixels[i]);
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}

//Jarvis, Judice and Ninke dithering
//   [ ] 7  5
// 3  5  7  5  3
// 1  3  5  3  1
private BitmapSource ErrorDiffusionJaJuNi(BitmapSource source)
{
    var wb = new WriteableBitmap(source);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    var pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);//byte配列にCopyPixel
    //float配列作成してコピー
    float[] iPixels = new float[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }
    long p = 0;//判定中ピクセルの配列の中での位置
    float gosa = 0;//誤差記録用もfloat
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + x;
            if (iPixels[p] < 127.5f)//127.5未満なら0(黒)
            {
                gosa = iPixels[p];//誤差記録
                iPixels[p] = 0;
            }
            else//127.5以上なら255(白)
            {
                gosa = (iPixels[p] - 255f);//誤差記録
                iPixels[p] = 255;
            }

            //誤差拡散
            if (p + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + 1] += (gosa / 48f) * 7f;//右
            }
            if (p + 2 < pixels.Length && x < w - 2)
            {
                iPixels[p + 2] += (gosa / 48f) * 5f;//2つ右
            }

            //下段
            if (y < h - 1)
            {
                if (x > 1)
                {
                    iPixels[p + stride - 2] += (gosa / 48f) * 3f;//2つ左
                }
                if (x != 0)
                {
                    iPixels[p + stride - 1] += (gosa / 48f) * 5f;//左
                }

                iPixels[p + stride] += (gosa / 48f) * 7f;//下

                if (x < w - 1)
                {
                    iPixels[p + stride + 1] += (gosa / 48f) * 5f;//右
                }
                if (x < w - 2)
                {
                    iPixels[p + stride + 2] += (gosa / 48f) * 3f;//2つ右
                }
            }

            //2行下
            if (y < h - 2)
            {
                long pp = p + (stride * 2);
                if (x > 1)
                {
                    iPixels[pp - 2] += (gosa / 48f) * 1f;//2つ左
                }
                if (x != 0)
                {
                    iPixels[pp - 1] += (gosa / 48f) * 3f;//左
                }

                iPixels[pp] += (gosa / 48f) * 5f;//下

                if (x < w - 1)
                {
                    iPixels[pp + 1] += (gosa / 48f) * 3f;//右
                }
                if (x < w - 2)
                {
                    iPixels[pp + 2] += (gosa / 48f) * 1f;//2つ右
                }
            }


            gosa = 0;//拡散させたので誤差をリセット
        }
    }
    //byte配列に戻す
    for (int i = 0; i < pixels.Length; ++i)
    {
        pixels[i] = (byte)(iPixels[i]);
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}

//アトキンソン
private BitmapSource ErrorDiffusionAtkinson(BitmapSource source)
{
    var wb = new WriteableBitmap(source);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    var pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);//byte配列にCopyPixel
    //float配列作成してコピー
    float[] iPixels = new float[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }
    long p = 0;//判定中ピクセルの配列の中での位置
    float gosa = 0;//誤差記録用もfloat
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + x;
            if (iPixels[p] < 127.5f)//127.5未満なら0(黒)
            {
                gosa = iPixels[p];//誤差記録
                iPixels[p] = 0;
            }
            else//127.5以上なら255(白)
            {
                gosa = (iPixels[p] - 255f);//誤差記録
                iPixels[p] = 255;
            }

            //誤差拡散
            if (p + 2 < pixels.Length && x < w - 2)
            {
                iPixels[p + 2] += (gosa / 8f) * 1f;//2つ右
            }
            if (p + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + 1] += (gosa / 8f) * 1f;//右隣+誤差(誤差拡散)                  
            }
            if (y < h - 1 && x != 0)
            {
                iPixels[p + stride - 1] += (gosa / 8f) * 1f;//左下
            }
            if (p + stride < pixels.Length)
            {
                iPixels[p + stride] += (gosa / 8f) * 1f;//真下
            }
            if (p + stride + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + stride + 1] += (gosa / 8f) * 1f;//右下
            }
            if (y < h - 2)
            {
                iPixels[p + stride * 2] += (gosa / 8f) * 1f;//2つ下
            }
            gosa = 0;//拡散させたので誤差をリセット
        }
    }
    //byte配列に戻す
    for (int i = 0; i < pixels.Length; ++i)
    {
        pixels[i] = (byte)(iPixels[i]);
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}
 
 
参照したところ
Libcaca study - 3. Error diffusion
http://caca.zoy.org/study/part3.html
 
 
 
 
関連記事
8色に減色でも誤差拡散法を試してみた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15385432.html
 
 
前回
誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15383023.html
 
 
2018/03/02は1週間後
単純減色と誤差拡散とディザリング ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15394008.html