午後わてんのブログ

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

単純減色と誤差拡散とディザリング

今回のアプリ
ダウンロード先
ここの20180302_.zip
 
前回の
単純減色に誤差拡散でディザリング
単純減色(ポスタライズ?)試してみた、WPFC#
https://blogs.yahoo.co.jp/gogowaten/15388558.html
これと
FloydSteinberg他いくつかの誤差拡散を試してみた、白黒2値をディザリング
https://blogs.yahoo.co.jp/gogowaten/15384380.html
これの組み合わせ
 
 
イメージ 2
いつものこの画像を変換
 
RGB各3階調で全27色に減色
イメージ 5
単純減色だけ
 
イメージ 3
単純減色+右隣への誤差拡散ディザ
ゴワゴワ感
 
イメージ 4
単純減色+フロイドスタインバーグ式誤差拡散
きれいにできている、元画像に近い
 
 
この前のパターンを使ったディザリングと比較
イメージ 1
RGB各3階調で全27色に減色
27色と言っても元画像に赤や緑がないから
使われているのは9色くらいだと思う
 
RGB各階調数ごとの変化
イメージ 6
右隣誤差拡散はあんまりきれいじゃないねえ
フロイドスタインバーグ式誤差拡散はどれもきれい
 
グラデーション画像の場合
イメージ 7
昨日のアプリで作った色相90のグラデーション画像
 
イメージ 8
右隣誤差拡散はすごい模様が出ているし赤色が目立つ
フロイドスタインバーグ式は中央の境目がはっきり
 
5階調
イメージ 9
同じ傾向だねえ
模様と横線の境目
 
8階調、512色
イメージ 10
この模様はかっこいいな
16階調、4096色
イメージ 11
グラデーション画像だと右隣誤差拡散はありだなあ
模様が逆にかっこいい
 
イメージ 12
RGB(255,244,240)から黒(0,0,0)のグラデーション
 
イメージ 13
RGB各色で誤差の蓄積でしきい値を超える場所が違うからだろうねえ
一方向のグラデーションだとわかりやすい
 

 
 
右隣誤差拡散

f:id:gogowaten:20191212002841p:plain

イメージ 16
4階調にする場合
1階調あたりの値は255/(階調数-1)=255/(4-1)=85
なので順番に
0, 85, 170, 255の4段階
すべてのピクセルの色の値をこのどれかに変換することになる
 
どれに決めるのかを閾値で判定
1階調当たりの閾値は255/階調数=255/4=63.75
なので順番に
0, 63.75, 127.5, 191.25, 255
0と255は使わないので実際は中の3つ
元の色が63.75未満なら0に変換
元の色が127.5未満なら85に変換
元の色が191.25未満なら170に変換
元の色が255未満なら255に変換
ってしたい
あとは特別に
元の色が0以下なら0に変換
元の色が255以上なら255に変換
 
変換前と変換後の差を誤差として、これを右隣のピクセルの色の値に足(拡散)していく
 
 
イメージ 17
元の色が0以下なら0に変換
元の色が255以上なら255に変換
しているところ、oldValueが元の色の値、、newValueが変換後の値、gosaが誤差記録用
 
 

f:id:gogowaten:20191212002915p:plain

125行目
(元の色の値/1階調当たりの閾値)の小数点切り捨ての値(倍率rate)を取得
元の色の値が150だったら150/63.75≒2.35=2
あとはこれに1階調当たりの値を掛けたものが変換後の値になる
130行目
85*2=170
 
131行目
誤差は150を170にしたので150-170=-20
 
128行目は元の色の値/1階調当たりの閾値がぴったり割り切れた時に
得られた値に-1している、これは閾値未満で分けるため
 
 
 
変換と誤差拡散
イメージ 19
133行目、新しい値が決まったので元の色の値をこれに変換
136行目、誤差を右隣のピクセルに足す(拡散)
134行目は右下(最後の)ピクセルの右隣はないのでそれを越えないように
 
 
フロイドスタインバーグ式誤差拡散

f:id:gogowaten:20191212002932p:plain

変換後の値を求めるのは右隣誤差拡散と同じ
誤差拡散法も1/1がx/16になって左下、真下、右下が増えただけ
このへんは以前の2階調限定のときと全く同じかな
なので他の誤差拡散法も同じようにできそう
 
 
処理速度
2048x1536の画像を変換
1秒 右隣誤差拡散
2秒 フロイドスタインバーグ式
階調数は関係ないみたい
一瞬で終わるパターンディザに比べると時間がかかるけど
思っていたよりは速い
 

f:id:gogowaten:20191212002946p:plain

画像右クリックから新しいタブで開くで見ると、きれいにディザリングされているのがわかる
 
 
デザイン画面

f:id:gogowaten:20191212003048p:plain

<Window x:Class="_20180302_単純減色と誤差拡散ディザ.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20180302_単純減色と誤差拡散ディザ"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>
    <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
      <Image Name="MyImage" UseLayoutRounding="True" Stretch="None"/>
    </ScrollViewer>
    <StackPanel Grid.Column="1">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="RGB各色数" VerticalAlignment="Center" Margin="4,0" FontSize="18"/>
        <TextBox Name="NumericTextBox" VerticalContentAlignment="Center" HorizontalContentAlignment="Right"
                 Text="{Binding ElementName=NumericScrollBar, Path=Value, UpdateSourceTrigger=PropertyChanged}"
                 Width="40" FontSize="18"/>
        <ScrollBar Name="NumericScrollBar" Value="3" Minimum="1" Maximum="256" SmallChange="1" LargeChange="1"
                   RenderTransformOrigin="0.5,0.5">
          <ScrollBar.RenderTransform>
            <RotateTransform Angle="180"/>
          </ScrollBar.RenderTransform>
        </ScrollBar>
        <TextBlock Name="TextBlockColorCount" Text="colorCount"/>
      </StackPanel>
      <Button Name="ButtonConvertErrorToRight" Content="右隣へ誤差拡散"/>
      <Button Name="ButtonConvertFloydSteinberg" Content="FloydSteinberg式誤差拡散"/>
      <Button Name="ButtonConvert" Content="ディザ無し"/>
      <Button Name="ButtonOrigin" Content="元の画像"/>
    </StackPanel>
  </Grid>
</Window>
 
C#コード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;

//単純減色と誤差拡散とディザリング(ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
//https://blogs.yahoo.co.jp/gogowaten/15394008.html

namespace _20180302_単純減色と誤差拡散ディザ
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    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;
            ButtonConvertErrorToRight.Click += ButtonConvertErrorToRight_Click;
            ButtonConvertFloydSteinberg.Click += ButtonConvertFloydSteinberg_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;
            //float f1 = -1.8f;
            //float f2 = -1.5f;
            //float f3 = -1.2f;
            //float f4 = -0.8f;
            //float f5 = -0.5f;
            //float f6 = -0.2f;
            //float gosa = 0f;
            //byte c = 0;
            //int rate = 0;
            //float freqcency = 255f / 2f;
            //for (int i = 0; i < 256; ++i)
            //{
            //    var neko = i / freqcency;
            //    rate = (int)Math.Floor(i / freqcency);
            //    c = (byte)(freqcency * rate);
            //    gosa = i - c;
            //}
        }

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

        /// <summary>
        /// 単純減色+右隣への誤差拡散
        /// 対象画像はPixelFormatがPbgra32のBitmapSource
        /// </summary>
        /// <param name="source">Pbgra32のBitmapSource</param>
        /// <param name="division">階調数、2から256を指定</param>
        private BitmapSource ErrorDiffusionToRight(BitmapSource source, int division)
        {
            //int division = (int)NumericScrollBar.Value;
            float frequencyThreshold = 255f / division;//1階調分の閾値
            //4階調のとき、0, 63.75, 127.5, 191.25, 255
            float frequencyValue = 255f / (division - 1f);//1階調分の値
            //4階調の時、0, 85, 170, 255

            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;//対象ピクセルの配列の中での位置
            int newValue;
            float gosa = 0, oldValue = 0, rate = 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)
                    {
                        oldValue = iPixels[p + i];//判定する値
                        //0以下なら0に変換、誤差はマイナス分そのものになる
                        if (oldValue <= 0)//未満じゃなくて以下が都合がいい
                        {
                            gosa = oldValue;
                            newValue = 0;
                        }
                        //255以上なら255に変換、超えた分が誤差になる
                        else if (oldValue >= 255)
                        {
                            gosa = oldValue - 255f;
                            newValue = 255;
                        }
                        //0より大きくて255未満の時
                        else
                        {
                            //何番目の閾値かを求める、値を閾値で割った整数部分、これが1階調分にかける倍率になる
                            rate = (int)Math.Floor(oldValue / frequencyThreshold);//Floorで切り捨てて整数部分取得
                            //閾値は未満と以上で分けたいので閾値ピッタリのときは倍率を一個下げるため
                            //割り切れたときは倍率を一個下げる
                            if (oldValue % frequencyThreshold == 0) { rate--; }
                            //変換後の値、1階調分に倍率をかけた値
                            newValue = (int)(frequencyValue * rate);
                            gosa = oldValue - newValue;
                        }
                        iPixels[p + i] = newValue;//変換
                        if (p + i + 4 < iPixels.Length)
                        {
                            iPixels[p + i + 4] += gosa;//誤差拡散
                        }
                    }
                }
            }
            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;
        }

        /// <summary>
        /// 単純減色+フロイドスタインバーグ式誤差拡散
        /// </summary>
        /// <param name="source">Pbgra32のBitmapSource</param>
        /// <param name="division">階調数、2から256を指定</param>
        /// <returns>Pbgra32のBitmapSource</returns>
        private BitmapSource FloydSteinberg(BitmapSource source, int division)
        {
            //int division = (int)NumericScrollBar.Value;
            float frequencyThreshold = 255f / division;//1階調分の閾値
            //4階調のとき、0, 63.75, 127.5, 191.25, 255
            float frequencyValue = 255f / (division - 1f);//1階調分の値
            //4階調の時、0, 85, 170, 255

            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
            //int配列作成してコピー
            float[] iPixels = new float[pixels.Length];
            for (int i = 0; i < iPixels.Length; ++i)
            {
                iPixels[i] = pixels[i];
            }

            long p = 0;
            float gosa = 0, oldValue = 0, newValue = 0, rate = 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)
                    {
                        oldValue = iPixels[p + i];//判定する値
                        //0以下なら0に変換、誤差はマイナス分そのものになる
                        if (oldValue <= 0)//未満じゃなくて以下が都合がいい
                        {
                            gosa = oldValue;
                            newValue = 0;
                        }
                        //255以上なら255に変換、超えた分が誤差になる
                        else if (oldValue >= 255)
                        {
                            gosa = oldValue - 255f;
                            newValue = 255;
                        }
                        //0より大きくて255未満の時
                        else
                        {
                            //何番目の閾値かを求める、値を閾値で割った整数部分、これが1階調分にかける倍率になる
                            rate = (int)Math.Floor(oldValue / frequencyThreshold);//Floorで切り捨てて整数部分取得
                            //閾値は未満と以上で分けたいので閾値ピッタリのときは倍率を一個下げるため
                            //割り切れたときは倍率を一個下げる
                            if (oldValue % frequencyThreshold == 0) { rate--; }
                            //変換後の値、1階調分に倍率をかけた値
                            newValue = (frequencyValue * rate);
                            gosa = oldValue - newValue;
                        }

                        iPixels[p + i] = newValue;//変換

                        //誤差拡散
                        if (p + i + 4 < iPixels.Length)
                        {
                            iPixels[p + i + 4] += (gosa / 16f) * 7f;//右隣
                        }

                        if (y < h - 1)//1行下
                        {
                            if (x != 0)
                            {
                                iPixels[p + i + stride - 4] += (gosa / 16f) * 3f;//左下
                            }
                            iPixels[p + i + stride] += (gosa / 16f) * 5f;//真下
                            if (x < w - 1)
                            {
                                iPixels[p + i + stride + 4] += (gosa / 16f) * 1f;//右下
                            }
                        }
                    }
                }
            }
            for (int i = 0; i < pixels.Length; ++i)
            {
                //pixels[i] = (byte)iPixels[i];//多分切り捨てになる
                pixels[i] = (byte)Math.Round(iPixels[i], MidpointRounding.AwayFromZero);//四捨五入
            }
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            return wb;
        }



        /// <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 ButtonConvertErrorToRight_Click(object sender, RoutedEventArgs e)
        {
            if (OriginBitmap == null) { return; }
            MyImage.Source = ErrorDiffusionToRight(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--; }
            NumericScrollBar.Value = (e.Delta > 0) ? NumericScrollBar.Value + 1 : NumericScrollBar.Value - 1;
        }

        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

        /// <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;
        }
    }
}
 
 
 
 
 
関連記事
 
2018/02/22
誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
2018/02/23
FloydSteinberg他いくつかの誤差拡散を試してみた、白黒2値をディザリング ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
2018/02/26
単純減色(ポスタライズ?)試してみた、WPFC# ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ