午後わてんのブログ

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

誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ

 
誤差拡散法を使ってディザリング
 
その中でも一番単純なもの
誤差を右隣のピクセルに拡散させるもの
さらにかんたんにするためにグレースケールの画像を使って試した
 
アプリダウンロード先

github.co

ここの20180221_.zipがそれ

 
 
イメージ 1
これが元画像
イメージ 2
変換結果
閾値は128で
0から128未満なら黒
128以上255以下なら白
 
左上ピクセルから右へ見ていって、閾値で白か黒に変換して、もとの値と変換後の差を右のピクセルに足していく
右端まで行ったら差をリセット(0に)して一段下がって左端からを繰り返す
 
ピクセルの値の並びが
100,90,120,250,60,20,150の場合

f:id:gogowaten:20191211231911p:plain

0,255,0,255,255,0,0に変換される
 
処理の流れ

f:id:gogowaten:20191211231930p:plain

100は128未満なので0に変換、差は100、これを右隣の90に足す
足して190は128以上なので255に変換、差は-45、これを右隣に足す
足して75は128未満なので0に変換、差は75、これを右隣に足す
これを右端に到達するまで繰り返す
右端に達したら差をリセット(0に)してから一段下がって同じことの繰り返し
 
 
これをC#で書く時に分かりづらかったのが
  • 右端に達して一段下がるときに差をリセットすること
  • 差が色の値の範囲0から255から溢れた場合にも溢れた分を切り捨てないで持ち越すこと
上の例だと差が溢れているのは4番目の250のところで
3番目で差が70、250+75=325、325は255から70溢れている
最初はこの溢れた70を切り捨てて、次のピクセル60はそのまま60で判定していた
 
 
 
 
渡されたグレースケールのBitmapsourceを右隣への誤差拡散法でディザリング
private BitmapSource Gosakakusan(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);

    long p = 0;//配列の中のピクセル位置
    int gosa = 0, v;
    for (int y = 0; y < h; ++y)
    {
        gosa = 0;//行が変わったら誤差をリセット
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + x;
            if (pixels[p] < 128)//128未満なら0(黒)
            {
                gosa += pixels[p];
                pixels[p] = 0;
            }
            else//128以上なら255(白)
            {
                gosa += (pixels[p] - 255);
                pixels[p] = 255;
            }

            //誤差拡散、右隣へ拡散
            if (p + 1 < pixels.Length && x < w - 1)
            {
                v = pixels[p + 1] + gosa;//右隣+誤差
                if (v < 0)
                {
                    gosa = pixels[p + 1] + gosa;//溢れた誤差を記録
                    v = 0;
                }
                else if (v > 255)
                {
                    gosa = pixels[p + 1] + gosa - 255;//溢れた誤差を記録
                    v = 255;
                }
                else
                {
                    gosa = 0;
                }
                pixels[p + 1] = (byte)v;//誤差拡散
            }
        }
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}
 
WriteableBitmapのCopyPixelで作った色の配列は方がbyte型で0以下や256以上の値は入らないので、誤差の記録用にint型の別の変数(gosa)
 
 
答え合わせは
 
イメージ 5
このグレースケール画像を変換したのを
 
Libcaca study - 3. Error diffusion
http://caca.zoy.org/study/part3.html
こちらの画像と比較してみる
 
イメージ 6
たぶんあっていると思う
 
勘違いしていた失敗例1
イメージ 7
左右ループってのは右端から左端へ行く時に誤差をリセットしないで
そのまま持っていくのを表している
切り捨ては0から255の範囲外に溢れた数値を切り捨てってこと
なのでこの結果は二重に間違った処理の結果
なんだけどこれはこれで、ありなんじゃないかなあって思うw
 
勘違いしていた失敗例2
イメージ 8
溢れたのは考慮するけどリセットなしの結果
これもありじゃないかなあ
 
勘違いしていた失敗例3
イメージ 9
惜しい、正解にかなり近い
リセットありだけど、溢れたのは切り捨て
 
普通の画像で
イメージ 10
右下が正解なんだけど
言われなきゃわからないねえ
 
イメージ 11
128から255のグラデーションの場合
 
イメージ 12
これは差が出た
右上のはリセット無しで切り捨てもなしだから
一行前の誤差がどんどん溜まっていって斜めになる
でもある意味誤差を最後のピクセルまで全く捨てていないから
全体としては一番正しいのかもw
 
 
2色
イメージ 13
右上が面白い
 
トマトの花と空
イメージ 14
下段のリセットなしの左右ループ組は画像の何もない左下に
縦の線が出てしまっている
右隣だけへの誤差拡散法は左右ループありのほうがいい感じだなあ
 
薄い背景の中央に濃い■
イメージ 15
これは右上はいまいちで正解の右下がいい
 
 
四角枠
イメージ 16
図形系は左右ループなしが無難な結果がでる
 
 
より引用
イメージ 18
誤差拡散って意味では天気図の等圧線みたいなのが出ていない右上がいいかなあ
 

参照したところ
誤差拡散法: koujinz blog
http://koujinz.cocolog-nifty.com/blog/2009/04/post-a316.html
ここがなければ今回のはできなかった
 
Libcaca study - 3. Error diffusion
http://caca.zoy.org/study/part3.html
 
 

 
 
この記事より古い関連記事
WPF、ディザパターンを使った白黒2値化 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15339223.html
 
次の記事
FloydSteinberg他いくつかの誤差拡散を試してみた、白黒2値をディザリング ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15384380.html
 
 
 
 
 
 
 

 
デザイン画面

f:id:gogowaten:20191211232310p:plain

 
コード
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;

namespace _20180221_誤差拡散
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmap;
        string ImageFileFullPath;

        public MainWindow()
        {
            InitializeComponent();
            this.Title = "右隣だけに誤差拡散、グレースケール";
            this.AllowDrop = true;
            this.Drop += MainWindow_Drop;
            Button0.Click += Button0_Click;
            //Button1.Click += Button1_Click;
            Button2.Click += Button2_Click;
            Button3.Click += Button3_Click;
            Button4.Click += Button4_Click;
            Button5.Click += Button5_Click;
        }


        private void Gosakakusan右隣だけ_左右ループ_切り捨て()
        {
            var wb = new WriteableBitmap(OriginBitmap);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            var pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);

            long p = 0;
            int v;
            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + x;
                    if (pixels[p] < 128)
                    {
                        v = pixels[p];
                        pixels[p] = 0;
                    }
                    else
                    {
                        v = pixels[p] - 255;
                        pixels[p] = 255;
                    }

                    if (p + 1 < pixels.Length)
                    {
                        v = pixels[p + 1] + v;
                        if (v < 0) { v = 0; }
                        else if (v > 255) { v = 255; }
                        pixels[p + 1] = (byte)v;
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            MyImage.Source = wb;
        }

        private void Gosakakusan右隣だけ_左右ループ_切り捨てなし()
        {
            var wb = new WriteableBitmap(OriginBitmap);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            var pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);

            long p = 0;
            int over = 0, v;
            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + x;
                    if (pixels[p] < 128)
                    {
                        over += pixels[p];
                        pixels[p] = 0;
                    }
                    else
                    {
                        over += (pixels[p] - 255);
                        pixels[p] = 255;
                    }

                    if (p + 1 < pixels.Length)
                    {
                        v = pixels[p + 1] + over;
                        if (v < 0)
                        {
                            over = pixels[p + 1] + over;
                            v = 0;
                        }
                        else if (v > 255)
                        {
                            over = pixels[p + 1] + over - 255;
                            v = 255;
                        }
                        else
                        {
                            over = 0;
                        }
                        pixels[p + 1] = (byte)v;
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            MyImage.Source = wb;
        }

        private void Gosakakusan右隣だけ_左右ループなし_切り捨て()
        {
            var wb = new WriteableBitmap(OriginBitmap);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            var pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);

            long p = 0;
            int v;
            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + x;
                    if (pixels[p] < 128)
                    {
                        v = pixels[p];
                        pixels[p] = 0;
                    }
                    else
                    {
                        v = pixels[p] - 255;
                        pixels[p] = 255;
                    }

                    if (x < w - 1 && p + 1 < pixels.Length)
                    {
                        v = pixels[p + 1] + v;
                        if (v < 0) { v = 0; }
                        else if (v > 255) { v = 255; }
                        pixels[p + 1] = (byte)v;
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            MyImage.Source = wb;
        }

        //これが正解、右端から左端への持ち越しはリセットする
        //持ち越した数値を足した時に0未満か256以上になった場合も溢れた数値を持ち越す
        private void Gosakakusan右隣だけ_左右ループなし_切り捨てなし()
        {
            var wb = new WriteableBitmap(OriginBitmap);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            var pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);

            long p = 0;
            int gosa = 0, v;
            for (int y = 0; y < h; ++y)
            {
                gosa = 0;//行が変わったら誤差をリセット
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + x;
                    if (pixels[p] < 128)//128未満なら0(黒)
                    {
                        gosa += pixels[p];
                        pixels[p] = 0;
                    }
                    else//128以上なら255(白)
                    {
                        gosa += (pixels[p] - 255);
                        pixels[p] = 255;
                    }

                    //誤差拡散、右隣へ拡散
                    if (p + 1 < pixels.Length && x < w - 1)
                    {                       
                        v = pixels[p + 1] + gosa;//右隣+誤差
                        if (v < 0)
                        {
                            gosa = pixels[p + 1] + gosa;//溢れた誤差を記録
                            v = 0;
                        }
                        else if (v > 255)
                        {
                            gosa = pixels[p + 1] + gosa - 255;//溢れた誤差を記録
                            v = 255;
                        }
                        else
                        {
                            gosa = 0;
                        }
                        pixels[p + 1] = (byte)v;//誤差拡散
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            MyImage.Source = wb;
        }




       #region イベント

        private void Button5_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            Gosakakusan右隣だけ_左右ループなし_切り捨てなし();
        }

        private void Button4_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            Gosakakusan右隣だけ_左右ループなし_切り捨て();
        }

        private void Button3_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            Gosakakusan右隣だけ_左右ループ_切り捨てなし();
        }

        private void Button2_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            Gosakakusan右隣だけ_左右ループ_切り捨て();
        }


        private void Button0_Click(object sender, RoutedEventArgs e)
        {
            MyImage.Source = OriginBitmap;
        }


        //画像ファイルドロップ時
        //PixelFormatをGray8(8bitグレースケール)に変換してBitmapSource取得
        private void MainWindow_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) { return; }
            string[] filePath = (string[])e.Data.GetData(DataFormats.FileDrop);
            OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Gray8, 96, 96);
            //OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Pbgra32, 96, 96);
            if (OriginBitmap == null)
            {
                MessageBox.Show("not Image");
            }
            else
            {
                MyImage.Source = OriginBitmap;
                ImageFileFullPath = System.IO.Path.GetFullPath(filePath[0]);//ファイルのフルパス保持
            }
        }
       #endregion

        private BitmapSource GetBitmapSourceWithChangePixelFormat2(
            string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
        {
            BitmapSource source = null;
            try
            {
                using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
                {
                    var bf = BitmapFrame.Create(fs);
                    var convertedBitmap = new FormatConvertedBitmap(bf, pixelFormat, null, 0);
                    int w = convertedBitmap.PixelWidth;
                    int h = convertedBitmap.PixelHeight;
                    int stride = (w * pixelFormat.BitsPerPixel + 7) / 8;
                    byte[] pixels = new byte[h * stride];
                    convertedBitmap.CopyPixels(pixels, stride, 0);
                    //dpi指定がなければ元の画像と同じdpiにする
                    if (dpiX == 0) { dpiX = bf.DpiX; }
                    if (dpiY == 0) { dpiY = bf.DpiY; }
                    //dpiを指定してBitmapSource作成
                    source = BitmapSource.Create(
                        w, h, dpiX, dpiY,
                        convertedBitmap.Format,
                        convertedBitmap.Palette, pixels, stride);
                };
            }
            catch (Exception)
            {

            }

            return source;
        }
    }
}
20時49分追記ここから
//Gosakakusanの改変
//誤差拡散時に0以下、256以上も扱えるようにint配列を作成して
//そこにコピーした値で誤差拡散処理をする
//すべてのピクセルの処理を終えたら元のbyte配列に戻す
private BitmapSource GosakakusanKai(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];
    //byte型配列にCopyPixel
    wb.CopyPixels(pixels, stride, 0);
    //int型配列作成してコピー
    int[] iPixels = new int[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }

    long p = 0;
    int gosa = 0;
    for (int y = 0; y < h; ++y)
    {
        gosa = 0;//行が変わったら誤差をリセット
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + x;
            if (iPixels[p] < 128)//128未満なら0(黒)
            {
                gosa += iPixels[p];//誤差記録
                iPixels[p] = 0;
            }
            else//128以上なら255(白)
            {
                gosa += (iPixels[p] - 255);//誤差記録
                iPixels[p] = 255;
            }

            //誤差拡散、右隣へ拡散
            if (p + 1 < pixels.Length && x < w - 1)
            {
                iPixels[p + 1] += gosa;//右隣+誤差(誤差拡散)
                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;
}
20時49分追記ここまで