午後わてんのブログ

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

WPF、ディザパターンを使った白黒2値化

 
ディザ、ディザリング
 
イメージ 1
輝度が0から255までのグラデーションのグレースケールを
白黒2値化すると中間で白と黒に分かれるので
元の画像とはかなり違うものになる
 
 
ディザリングを使うと
イメージ 2
同じ白黒2値でも元の画像に近くなる
なにより見た目がかっこいい!!
 
 
イメージ 3
灰色を白黒2値で表現するには白と黒の割合が同じになるように並べると
それっぽく見える
 
ここからエクセル方眼紙
ディザパターン

f:id:gogowaten:20191211221940p:plain

輝度によって白と黒の割合を変える、なるべく偏らないように並べる
 
 
 
イメージ 5
横12ピクセル、縦2ピクセルの画像、数値は輝度
これをパターンに当てはめて2値化すると
 
イメージ 6
こうなる
 
 
イメージ 7
2x2マスのディザパターンを使うから
画像も2x2の4マスに分けて考える
 
 
イメージ 8
輝度93は51~102の間なのでディザパターンbになる
上の93はマスの左上、パターンbの左上は白なので白判定
下の93はマスの左下、パターンbの左下は黒なので黒判定
輝度116はパターンc、当てはめると
上の116は黒、下の116は白
結果、このマスは
イメージ 9
こうなればいい
 
 
2x2に当てはまらない画像のとき
イメージ 10
5x5とか奇数のピクセルの時
 
イメージ 11
左上から右へ2x2で考える
 
イメージ 12
右下の輝度値195なら
195はパターンdでdの左上は白なので白判定になる
 
イメージ 24
だいたいこんな感じ
 
 
正規化

f:id:gogowaten:20191211222026p:plain

輝度値は0~255で指定するけどしきい値にするには計算しにくいので0~1に変換して計算する
しきい値を4つ指定すると全体を5分割できることになるから
5段階の表現ができる
 
ここまでエクセル方眼紙
 
イメージ 14
5段階の表現
 
 

f:id:gogowaten:20191211222038p:plain

 
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;


namespace _20180123_パターンディザ2x2白黒2値
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmap;

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

            ButtonOrigin.Click += ButtonOrigin_Click;
            ButtonTest1.Click += ButtonTest1_Click;
            ButtonTest2.Click += ButtonTest2_Click;
            ButtonTest3.Click += ButtonTest3_Click;
            ButtonNotDithering.Click += ButtonNotDithering_Click;
            NumericScrollBar.ValueChanged += NumericScrollBar_ValueChanged;
            NumericTextBox.TextChanged += NumericTextBox_TextChanged;
        }

        //ディザなしボタンクリック時
        private void ButtonNotDithering_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MatrixThreshold();
        }





        //     正規表現の基本 - .NET Tips(VB.NET, C#...)
        //https://dobon.net/vb/dotnet/string/regex.html

        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 NumericScrollBar_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (OriginBitmap == null) { return; }
            MatrixThreshold();
        }

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

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

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

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

        //画像ファイルドロップ時
        //グレースケールに変換して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);
            //OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Bgr24, 96, 96);
            OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Gray8, 96, 96);
            //OriginBitmap = GetBitmapSourceWithChangePixelFormat2(filePath[0], PixelFormats.Rgb24, 96, 96);

            if (OriginBitmap == null)
            {
                MessageBox.Show("not Image");
            }
            else
            {
                MyImage.Source = OriginBitmap;
            }
        }

        //ディザなし、しきい値指定で白黒2値化
        private void MatrixThreshold()
        {
            double[][] thresholdMap = new double[][]
            {
                new double[] { NumericScrollBar.Value / 255 }
            };
            DitheringGrayScale(thresholdMap);
        }


        //2x2ディザ
        private void Matrix1_Bayer2x2()
        {
            double[][] thresholdMap = new double[][]
            {
                new double[] { 1f / 5f, 3f / 5f },
                new double[] { 4f / 5f, 2f / 5f }
            };
            DitheringGrayScale(thresholdMap);
        }

        //4x4ディザ
        private void Matrix2_Bayer4x4()
        {
            double[][] thresholdMap = new double[][]
            {
                new double[] { 1f / 17f, 13f / 17f, 4f / 17f, 16f / 17f },
                new double[] { 9f / 17f, 5f / 17f, 12f / 17f, 8f / 17f },
                new double[] { 3f / 17f, 15f / 17f, 2f / 17f, 14f / 17f },
                new double[] { 11f / 17f, 7f / 17f, 10f / 17f, 6f / 17f }
            };
            DitheringGrayScale(thresholdMap);
        }

        //2x2ディザの変則
        private void Matrix3_Bayer2x2()
        {
            double[][] thresholdMap = new double[][]
            {
                new double[] { 1f / 5f, 3f / 5f },
                new double[] { 2f / 5f, 4f / 5f }
            };
            DitheringGrayScale(thresholdMap);
        }







        //ディザパターン(行列)を使って白黒2値化
        //8bppGrayScaleの画像に対応
        private void DitheringGrayScale(double[][] thresholdMap)
        {
            var wb = new WriteableBitmap(OriginBitmap);
            int h = wb.PixelHeight;
            int w = wb.PixelWidth;
            //strideは1ピクセル行のbyte数
            //8bppのGrayScale画像は1ピクセル1byteだからstride = 1行のピクセル数になる
            int stride = w;// wb.BackBufferStride;
            byte[] pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);
            long p = 0;
            int xx = thresholdMap[0].Length;//しきい値行列の横の要素数
            int yy = thresholdMap.Length;   //しきい値行列の縦の要素数
            for (int y = 0; y < h; ++y)
            {
                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + x;
                    if (thresholdMap[y % yy][x % xx] <= (double)pixels[p] / 255)//255
                    { pixels[p] = 255; }
                    else { pixels[p] = 0; }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            MyImage.Source = wb;
        }



        /// <summary>
        ///  ファイルパスとPixelFormatを指定してBitmapSourceを取得、dpiの変更は任意
        /// </summary>
        /// <param name="filePath">画像ファイルのフルパス</param>
        /// <param name="pixelFormat">PixelFormatsの中からどれかを指定</param>
        /// <param name="dpiX">無指定なら画像ファイルで指定されているdpiになる</param>
        /// <param name="dpiY">無指定なら画像ファイルで指定されているdpiになる</param>
        /// <returns></returns>
        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;
        }
    }
}
 
 
画像ファイルのドロップで画像を開く
カラーの画像はグレースケールに変換したものが表示される
Testボタン 2x2のディザパターン
Test2 4x4ディザパターン
Test3 変則的な2x2ディザパターン
 
 
 
イメージ 16
これが
イメージ 17
このディザパターンの行列を作っているところ
って今見たら0.8と0.6の指定が逆になっている
けど対角線上で入れ替わっているだけで偏っていないから問題ない
 
 
ディザパターンを使って白黒2値化
イメージ 18
WriteableBitmapのCopyPixelsで色(輝度)情報を配列にコピーして
ディザパターンの行列を使って白黒2値化したら
WriteableBitmapのWritePixelsで配列を書き込んで
Imageに表示
 
白黒判定は
if (thresholdMap[y % yy][x % xx] <= (double)pixels[p] / 255)
ここ
pは今のピクセル位置を配列の位置に変化したもの
y % yyの%は割り算の余りを求めている
yは縦のピクセル位置
yyは行列の縦の数なので2で固定
yが0のときは0%2=0、yが1のときは1%2=1、yが2のときは2%2=0とかになる
x%xxは横で同じことをしている
これで今のピクセルの輝度をどのしきい値で判定すればいいかがわかる
 
ディザパターンは2x2の
0.2  0.6
0.8  0.4
こうなので縦の行列数2、横の行列数2
ピクセル位置(3, 4)の輝度値184のときなら
if (thresholdMap[y % yy][x % xx] <= 184 / 255)
y%yy = 3%2 = 1
x%xx = 4%2 = 0
if (thresholdMap[1][0] <= 184 / 255)
行列は0から数えるので、縦1、横0は0.8
if (0.8 <= 184 / 255)
輝度値184を正規化して、184/255=0.72
しきい値と比較
if (0.8 <= 0.72)
これはfalse、しきい値以下なので
else { pixels[p] = 0; }
ピクセル位置(3, 4)の輝度値184は黒判定(0)になる
 
 
 
イメージ 19
左:元画像、右:ディザなし
イメージ 20
左:2x2ディザ、右:4x4ディザ
どちらもそれぞれの良さがある
変則的な2x2ディザパターン
イメージ 21
同じ2x2でも縦のシマシマが目立つ
イメージ 22
通常は上下左右均等的だけど変則の方は
左右でしきい値が分かれる感じなので
縦縞が出るようになるみたい
おもしろい
エクセル方眼紙も最高なんだよなあ

f:id:gogowaten:20191211222317p:plain

画像を右クリックして新しいタブで開くで、もとの大きさで表示すると
きれいなパターンになっているのがわかる

2018/06/02追記
変換した画像を保存できるようにしてみた
イメージ 25
選べる保存形式はpng,bmp,tiffの3形式
 
イメージ 27
ビットの深さは1(1bpp)

f:id:gogowaten:20200318190948p:plain

 
ダウンロード
追記ここまで

 
 
 
参照したところ
2値化して、1bppの白黒画像を作成する - .NET Tips (VB.NET,C#...)
https://dobon.net/vb/dotnet/graphics/1bpp.html
ディザパターンを多段配列にして使う方法はここから
こういう方法を思いつく人はすごいと思う
 
Libcaca study - 2. Halftoning
http://caca.zoy.org/study/part2.html
英語だけど色んなパターンが紹介されている
 
ディザリング(パターン・ディザ): koujinz blog
http://koujinz.cocolog-nifty.com/blog/2009/04/post-ebdd.html
ここの説明がわかりやすかった
 
 
 
 
関連記事
前回は2日前
カラー画像を1bpp(1bit)白黒画像に変換して保存するアプリ作ってみた、しきい値は手動設定 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15335812.html
 
次回は5日後
WPF、画像をディザパターンを使って8色に減色して保存するアプリ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15346592.html
 
 
1ヶ月後
誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15383023.html