午後わてんのブログ

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

単純減色(ポスタライズ)にオーダード(パターン)ディザリング、WPFとC#

 
単純減色(ポスタライズ)にオーダード(パターン)ディザリングしてみた
つまり
WPF、画像をディザパターンを使って8色に減色して保存するアプリ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15346592.html
これプラス
 
単純減色(ポスタライズ?)試してみた、WPFC# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15388558.html
これ
 
イメージ 1
パターンディザのパターンはbayer型で
2x2は
1 3
4 2
を5で割った数値
0.2 0.6
0.8 0.4
これが閾値になる
 
4x4は
1 13 4 16
9 5 12 8
3 15 2 14
11 7 10 6
を17で割った数値
 
 
閾値を求める

f:id:gogowaten:20191212000023p:plain

色の値0から255を2階調にする場合
255を2で割った127.5を閾値にして0か255の2階調にする
この255を2で割ったっていうのが0.5、これが閾値閾値になる、閾値の基準?かな
255*0.5=127.5
単純減色はこれの繰り返しになる、ずーっと0.5
 
2階調で閾値が0.4の場合

f:id:gogowaten:20191212000042p:plain

2x2のディザパターンの右下ピクセルが0.4なのでこれに当たる
255*0.4=102
0から102未満を0、102以上は255
 
 
4階調で閾値0.5

f:id:gogowaten:20191212000125p:plain

0.5なら同じ間隔が並ぶだけ
閾値は255を階調数で割った、255/4=63.75間隔
色は255を階調数-1で割った、255/(4-1)=85間隔
(閾値間隔が画像では64なのは255じゃなくて256で計算しているから)
 
4階調で閾値0.4

f:id:gogowaten:20191212000141p:plain

0.5のとき閾値間隔は255/4=63.75間隔なので
0.4なら63.75/0.5*0.4=51、これが最初の閾値になる
残りの0.1は次の色(85)になるを繰り返すので直すと
 

f:id:gogowaten:20191212000206p:plain

こうなる
最後の255の範囲は広がることになる
0.5と比べると左に0.1ずれただけだねえ
 
閾値0.6なら

f:id:gogowaten:20191212000220p:plain

最初の閾値が63.75/0.5*0.6=76.5
0.5と比べると右にずれた状態
こんな感じで最初の閾値がわかれば、あとは閾値間隔は同じなので足していけば良さそうで、最後の255の範囲はつじつま合わせすれば良さそうってことで
できたのが
 
デザイン画面 MainWindow

f:id:gogowaten:20191212000231p:plain

 
C#コード
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;


namespace _20180227_単純減色にパターンディザ
{
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmap;
        string ImageFileFullPath;

        public MainWindow()
        {
            InitializeComponent();
            this.Title = this.ToString();
            this.AllowDrop = true;
            this.Drop += MainWindow_Drop;
            ButtonConvert.Click += ButtonConvert_Click;

            ButtonConvertWith1x1Dither.Click += ButtonConvertWith1x1Dither_Click;
            ButtonConvertWith2x2Dither.Click += ButtonConvertWith2x2Dither_Click;
            ButtonConvertWith4x4Dither.Click += ButtonConvertWith4x4Dither_Click;
            ButtonOrigin.Click += ButtonOrigin_Click;
            NumericScrollBar.ValueChanged += NumericScrollBar_ValueChanged;
            NumericScrollBar.MouseWheel += NumericScrollBar_MouseWheel;
            NumericTextBox.MouseWheel += NumericTextBox_MouseWheel;
            NumericTextBox.GotFocus += NumericTextBox_GotFocus;
            NumericTextBox.TextChanged += NumericTextBox_TextChanged;

        }


        //2x2ディザパターンのしきい値行列
        private float[][] Get2x2ditherMatrix()
        {
            return new float[][]
            {
                new float[]{ 1f / 5f, 3f / 5f },
                new float[]{ 4f / 5f, 2f / 5f }
            };
        }

        //ディザパターンなし
        private float[][] Get1x1ditherMatrix()
        {
            return new float[][] { new float[] { 1f / 2f } };
        }

        //4x4ディザパターンのしきい値行列
        private float[][] Get4x4ditherMatrix()
        {
            return new float[][] {
                new float[] { 1f / 17f, 13f / 17f, 4f / 17f, 16f / 17f },
                new float[] { 9f / 17f, 5f / 17f, 12f / 17f, 8f / 17f },
                new float[] { 3f / 17f, 15f / 17f, 2f / 17f, 14f / 17f },
                new float[] { 11f / 17f, 7f / 17f, 10f / 17f, 6f / 17f }
            };
        }

        /// <summary>
        /// 単純減色にディザパターンを使う
        /// </summary>
        /// <param name="ditherMatrix">ディザパターン、中の数値は0から1を指定</param>
        /// <param name="source">変換する画像</param>
        /// <param name="division">階調数、各RGBの分割数、2から256を指定</param>
        private BitmapSource SimpleGensyokuWithPatternDither(float[][] ditherMatrix, BitmapSource source, int division)
        {
            //変換対応表取得
            byte[][][] converter = GetConverterArray4(division, ditherMatrix);
            //converter[][][]
            //index[ディザパターンの縦位置] [横位置] [変換前の値]
            //value[処理ピクセルの縦位置] [横位置] [変換後の値]

            var wb = new WriteableBitmap(source);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            byte[] pixles = new byte[h * stride];
            wb.CopyPixels(pixles, stride, 0);
            long p = 0;//処理ピクセルの配列の中での位置
            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + (x * 4);
                    //対応表に当てはめて色変換
                    for (int i = 0; i < 3; ++i)//RGB各色
                    {
                        pixles[p + i] = converter[y % converter.Length][x % converter[0].Length][pixles[p + i]];
                        //var neko = x % converter[0].Length;
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixles, stride, 0);
            return wb;
        }



        /// <summary>
        /// 階調数とディザパターンの閾値から変換一覧作成
        /// </summary>
        /// <param name="division">階調数</param>
        /// <param name="threshold">ディザパターンの閾値</param>
        /// <returns></returns>
        private byte[] GetConverterArray3_2(int division, float threshold)
        {
            //階調数4でディザパターン行列の閾値が0.2のとき
            //値は255/(4-1)=85が単位になるので0,85,170,255
            //最初の閾値は255/階調数/0.5*行列の閾値なので
            //255/4/0.5*0.2=25.5
            //続く閾値はこれに255/4=63.75を足していく
            //25.5、89.25、153、216.75

            //閾値一覧作成
            float[] range = new float[division + 1];
            float threshold最初 = (float)(255f / division / 0.5 * threshold);
            range[0] = threshold最初;
            for (int i = 1; i < range.Length; ++i)
            {
                range[i] = threshold最初 + (255f / division * i);
            }

            //指定値一覧作成
            float colorFrequency = 255f / (division - 1f);//色の指定値の間隔
            byte[] color = new byte[division + 1];//一個余裕を持たせて最後には255を入れておく
            for (int i = 0; i < color.Length; ++i)
            {
                if (i * colorFrequency >= 255)
                {
                    color[i] = 255;
                }
                else
                {
                    //color[i] = (byte)(i * colorFrequency);
                    //四捨五入してからbyteにキャストォォぉ
                    color[i] = (byte)Math.Round(
                        (i * colorFrequency), MidpointRounding.AwayFromZero);
                }
            }

            //変換一覧作成
            byte[] neko = new byte[256];
            int thresholdIndex = 0;//閾値一覧のindex
            for (int i = 0; i < neko.Length; ++i)
            {
                //しきい値を超えたら次の閾値に変更する
                if (i > range[thresholdIndex])
                {
                    thresholdIndex++;
                }
                neko[i] = color[thresholdIndex];
            }

            return neko;
        }

        //閾値行列から変換一覧作成
        private byte[][][] GetConverterArray4(int division, float[][] ditherMatrix)
        {
            byte[][][] converterArray = new byte[ditherMatrix.Length][][];
            for (int i = 0; i < ditherMatrix.Length; ++i)
            {
                converterArray[i] = new byte[ditherMatrix[i].Length][];
                for (int j = 0; j < ditherMatrix[i].Length; ++j)
                {
                    converterArray[i][j] = GetConverterArray3_2(division, ditherMatrix[i][j]);
                }
            }

            return converterArray;
        }



        /// <summary>
        /// 単純減色の変換対応表作成
        /// </summary>
        /// <param name="division">分割数(階調数)</param>
        /// <returns></returns>
        private byte[] GetConverterArray(int division)
        {
            //範囲           
            float frequency = 256f / division;//1範囲の大きさ、周波数
            float[] range = new float[division + 1];//3分割なら0,85,170,255になる
            for (int i = 0; i < range.Length; ++i)
            {
                range[i] = i * frequency;
            }

            //指定する値
            frequency = 255f / (division - 1);
            byte[] color = new byte[division];//3分割なら0,127,255になる
            for (int i = 0; i < color.Length; ++i)
            {
                color[i] = (byte)(i * frequency);
            }

            //元の256階調全てに対する変換結果の配列作成、対応表
            byte[] converter = new byte[256];
            int j = 0;
            for (int i = 0; i < 256; ++i)
            {
                if (i >= range[j + 1])
                {
                    j++;
                }
                converter[i] = color[j];
            }
            return converter;
        }
        //     ポスタリゼーション(階調変更)
        //http://www.sm.rim.or.jp/~shishido/post.html
        //対応表を作成しておいて、それに当てはめて判定、速い
        private void GensyokuNumeric2Table()
        {
            int division = (int)NumericScrollBar.Value;//分割数
            //変換対応表取得
            byte[] converter = GetConverterArray(division);

            var wb = new WriteableBitmap(OriginBitmap);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            int stride = wb.BackBufferStride;
            byte[] pixles = new byte[h * stride];
            wb.CopyPixels(pixles, stride, 0);
            long p = 0;

            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + (x * 4);
                    //対応表に当てはめて色変換
                    pixles[p + 2] = converter[pixles[p + 2]];
                    pixles[p + 1] = converter[pixles[p + 1]];
                    pixles[p + 0] = converter[pixles[p + 0]];
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixles, stride, 0);
            MyImage.Source = wb;
            TextBlockColorCount.Text = Math.Pow(division, 3).ToString();
        }



       #region イベント


        private void ButtonConvertWith4x4Dither_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = SimpleGensyokuWithPatternDither(Get4x4ditherMatrix(), OriginBitmap, (int)NumericScrollBar.Value);
        }

        private void ButtonConvertWith2x2Dither_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = SimpleGensyokuWithPatternDither(Get2x2ditherMatrix(), OriginBitmap, (int)NumericScrollBar.Value);
        }

        private void ButtonConvertWith1x1Dither_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = SimpleGensyokuWithPatternDither(Get1x1ditherMatrix(), OriginBitmap, (int)NumericScrollBar.Value);
        }

        private void NumericTextBox_GotFocus(object sender, RoutedEventArgs e)
        {
            TextBox box = (TextBox)sender;
            this.Dispatcher.InvokeAsync(() => { Task.Delay(10); box.SelectAll(); });
        }

        private void NumericScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            TextBlockColorCount.Text = Math.Pow(NumericScrollBar.Value, 3).ToString();
        }



        private void ButtonConvert_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            GensyokuNumeric2Table();
        }

        private void NumericTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            TextBox textBox = (TextBox)sender;
            double d;
            if (!double.TryParse(textBox.Text, out d))
            {
                textBox.Text = System.Text.RegularExpressions.Regex.Replace(textBox.Text, "[^0-9]", "");
            }
        }


        private void NumericTextBox_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0) { NumericScrollBar.Value++; }
            else { NumericScrollBar.Value--; }
        }

        private void NumericScrollBar_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0) { NumericScrollBar.Value++; }
            else { NumericScrollBar.Value--; }
        }

        private void ButtonOrigin_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = OriginBitmap;
        }


        //画像ファイルドロップ時
        //PixelFormat.Pbgr32に変換して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.Pbgra32, 96, 96);

            if (OriginBitmap == null)
            {
                MessageBox.Show("not Image");
            }
            else
            {
                MyImage.Source = OriginBitmap;
                ImageFileFullPath = 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;
        }

    }
}
 
 
 
 
2x2のディザパターンの行列作成
イメージ 8
0.2 0.6
0.8 0.4
になっている
 
 
ディザパターンと単純減色する
SimpleGensyokuWithPatternDither
ディザパターンの閾値行列と画像と階調数を渡す
画像のPixelFormatはPbgr32限定
イメージ 9
いつものWriteableBitmapのCopyPixelでRGBの値を配列に入れて、その値を変更する
一昨日の単純減色と同じように変換一覧作成する、違うのは閾値の数だけ作成することで
2x2のパターンなら4つ作成する
85行目のところ
byte converter = GetConverterArray4(division, ditherMatrix);
 
 
GetConverterArray4
イメージ 10
ここから
239行目で実際の一覧作成するGetConverterArray3_2に
行列の中の数値を順番に渡して、閾値の個数分の一覧を作成している
converterArray[i][j] = GetConverterArray3_2(division, ditherMatrix[i][j]);
 
 
今回の要
GetConverterArray3_2
イメージ 11
閾値を求める時に階調数で割る値は
256の気がするんだけど255のほうが
期待どおりになったからそうしているだけでよくわかっていない
なので正確じゃないかも
 
 
2階調、ディザパターン2x2の場合
イメージ 12
GetConverterArray4の処理の途中
2x2のパターン
0.2 0.6
0.8 0.4
がある、この閾値それぞれの変換一覧を作成する
0.2から作成
GetConverterArray3に渡す
 
GetConverterArray3
イメージ 13
階調数2で閾値0.2の最初の閾値
255/2/0.5*0.2=51
の計算が終わったところ
 
閾値0.2の閾値一覧作成
イメージ 14
最初の閾値51に
255/階調数の255/2=127.5を足していって
51,178.5,306
っていう一覧ができたところ
 
指定する色の値の一覧作成
イメージ 15
2階調なので0か255の2択でこうなる
さっきの閾値一覧で255を超えたときのために
最後は余分に255を入れている
 
閾値一覧と色の値の一覧を使って変換一覧作成
イメージ 16
長さ256の配列nekoに順番に色の値を入れていく
最初の閾値51を超えたところ
次の色の値を入れたいので色の値の一覧のindexを増やす直前
 
閾値が51, 178.5, 306
色値が0, 255, 255
なので
0から51までが 0
52から179までが 255
179から255までが 255
になるはず
 
イメージ 17
52番めに2番目の色の値255が入ったところ
 
イメージ 18
2つめの閾値178.5を超えたところ
次の色の値も255なのでこのまま最後まで255が入る
 
0.2用の変換一覧作成完了
イメージ 19
最後まで値が入った
 
イメージ 20
GetConverterArray4に戻ってきたところ
続けて0.6,0.8,0.4も作成されて
 
イメージ 21
2x2のディザパターン用の変換一覧作成したところ
4つの一覧ができた
 
 
イメージ 22
閾値0.6のしきいは
255/2/0.5*0.6=153
 
イメージ 23
閾値0.8
255/2/0.5*0.8=204
 
イメージ 24
閾値0.4
255/2/0.5*0.4=102
 
 
イメージ 25
SimpleGensyokuWithPatternDitherに戻ってきたところ
 
変換一覧を使って変換
イメージ 26
105行目
xとyはピクセルの位置、pはそのピクセルの色情報の配列の中の位置
今は最初なのでどれも0なので処理する値はpixels[0]の155
使う変換一覧はconverterに[y][x][値]を当てはめてその中の値が目的の値になる
y % converter.Lengthはyをconverterの要素数で割った余りを求めている
これでピクセルの位置がディザパターンのどの位置に当たるのかがわかる
素数はディザパターンの行列の数なので2x2なら2
0.2 0.6
0.8 0.4
ピクセルの位置y、x=(5, 4)なら(5%2, 4%2)=(1, 0)なので
行列だと左下なので0.8の変換一覧を使うことになる
 
最初のピクセル位置は0,0
(0%2, 0%2)=(0, 0)なので
ディザパターンだと左上の0.2の変換一覧を使うことになる
converter[0][0][155]の中は
イメージ 27
255
 
 
イメージ 28
pixels[0]の155は255に変換された
PixelFormat.Pbgr32はBGRAの順番に並んでいるので、この場合はB青の値155が255になったことになる
次のpixels[1]75はG緑、[2]は赤、[3]は不透明度
ここまでは同じピクセルなので同じ変換表を使う、といっても不透明度は255のまま
0.2の変換一覧は
イメージ 29
52以上が255になっているので
 
結果
イメージ 30
75の緑は255
34の赤は0
 
 
次のピクセルは右隣で座標はy,x=(0, 1)なので
(0%2, 1%2)= (0, 1)で右上の0.6用の一覧を使う
配列の場所はpixels[4]からpixels[7]
 
イメージ 31
0.6用
 
 
イメージ 32
153と154が境目
 
変換するRGBの値
イメージ 33
 
結果
イメージ 34
変換された
これを最後のピクセルまで繰り返して
イメージ 35
2階調なので0か255に変換された
 

 
1x1の行列、閾値0.5
イメージ 36
閾値行列を1x1、閾値1/2=0.5にすると
すべてのピクセル閾値0.5で処理されるので
ディザなしの単純減色と同じ結果になる
イメージ 37

 
イメージ 38
いいねえ
 
イメージ 39
イメージ 40
ここまで色数を増やすと2x2も4x4も変わらないなあ
 
256階調
イメージ 41
64階調くらいから元の画像と見分けつかないのは
ディザなしでも同じ
 
256階調のときの変換一覧
イメージ 42
 
イメージ 43
256階調を256階調に変換だから
これであっているはず
 
イメージ 44
イメージ 45
いいねえ
 
 
イメージ 46
色相16のSV画像
 
イメージ 47
こうなるんだなあ
 
イメージ 48

f:id:gogowaten:20191212000446p:plain

 
 
今回のは難しくてギリギリだったけどできたなあ
処理速度も一番時間の掛かりそうな256階調+パターンディザ4x4で
2048x1536ピクセルの画像でも一瞬(0.2秒くらい)で終わる速さで満足
 
 
まだ使い方わからんけど、フォルダごとアップロードができるのはわかった
 
アプリダウンロード先
ここの20180227_.zip
 
 
 
 
関連記事
一年後の2019/02/26
任意の2色に減色するときディザリングパターンを使う ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15883853.html
RGBの階調じゃなくて任意の色でのディザリングパターン